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
402async fn get_docs_handler() -> axum::response::Html<&'static str> {
404 axum::response::Html(include_str!("../static/docs.html"))
405}
406
407pub async fn build_router(
409 spec_path: Option<String>,
410 options: Option<ValidationOptions>,
411 failure_config: Option<FailureConfig>,
412) -> Router {
413 build_router_with_multi_tenant(
414 spec_path,
415 options,
416 failure_config,
417 None,
418 None,
419 None,
420 None,
421 None,
422 None,
423 None,
424 )
425 .await
426}
427
428fn apply_cors_middleware(
430 app: Router,
431 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
432) -> Router {
433 use http::Method;
434 use tower_http::cors::AllowOrigin;
435
436 if let Some(config) = cors_config {
437 if !config.enabled {
438 return app;
439 }
440
441 let mut cors_layer = CorsLayer::new();
442 let is_wildcard_origin;
443
444 if config.allowed_origins.contains(&"*".to_string()) {
446 cors_layer = cors_layer.allow_origin(Any);
447 is_wildcard_origin = true;
448 } else if !config.allowed_origins.is_empty() {
449 let origins: Vec<_> = config
451 .allowed_origins
452 .iter()
453 .filter_map(|origin| {
454 origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
455 })
456 .collect();
457
458 if origins.is_empty() {
459 warn!("No valid CORS origins configured, using permissive CORS");
461 cors_layer = cors_layer.allow_origin(Any);
462 is_wildcard_origin = true;
463 } else {
464 if origins.len() == 1 {
467 cors_layer = cors_layer.allow_origin(origins[0].clone());
468 is_wildcard_origin = false;
469 } else {
470 warn!(
472 "Multiple CORS origins configured, using permissive CORS. \
473 Consider using '*' for all origins."
474 );
475 cors_layer = cors_layer.allow_origin(Any);
476 is_wildcard_origin = true;
477 }
478 }
479 } else {
480 cors_layer = cors_layer.allow_origin(Any);
482 is_wildcard_origin = true;
483 }
484
485 if !config.allowed_methods.is_empty() {
487 let methods: Vec<Method> =
488 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
489 if !methods.is_empty() {
490 cors_layer = cors_layer.allow_methods(methods);
491 }
492 } else {
493 cors_layer = cors_layer.allow_methods([
495 Method::GET,
496 Method::POST,
497 Method::PUT,
498 Method::DELETE,
499 Method::PATCH,
500 Method::OPTIONS,
501 ]);
502 }
503
504 if !config.allowed_headers.is_empty() {
506 let headers: Vec<_> = config
507 .allowed_headers
508 .iter()
509 .filter_map(|h| h.parse::<http::HeaderName>().ok())
510 .collect();
511 if !headers.is_empty() {
512 cors_layer = cors_layer.allow_headers(headers);
513 }
514 } else {
515 cors_layer =
517 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
518 }
519
520 let should_allow_credentials = if is_wildcard_origin {
524 false
526 } else {
527 config.allow_credentials
529 };
530
531 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
532
533 info!(
534 "CORS middleware enabled with configured settings (credentials: {})",
535 should_allow_credentials
536 );
537 app.layer(cors_layer)
538 } else {
539 debug!("No CORS config provided, using permissive CORS for development");
543 app.layer(CorsLayer::permissive().allow_credentials(false))
546 }
547}
548
549#[allow(clippy::too_many_arguments)]
551pub async fn build_router_with_multi_tenant(
552 spec_path: Option<String>,
553 options: Option<ValidationOptions>,
554 failure_config: Option<FailureConfig>,
555 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
556 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
557 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
558 ai_generator: Option<Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>>,
559 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
560 mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
561 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
562) -> Router {
563 use std::time::Instant;
564
565 let startup_start = Instant::now();
566
567 let mut app = Router::new();
569
570 let mut rate_limit_config = middleware::RateLimitConfig {
573 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
574 .ok()
575 .and_then(|v| v.parse().ok())
576 .unwrap_or(1000),
577 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
578 .ok()
579 .and_then(|v| v.parse().ok())
580 .unwrap_or(2000),
581 per_ip: true,
582 per_endpoint: false,
583 };
584
585 let mut final_cors_config = cors_config;
587 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
588 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
590
591 if let Some(deploy_config) = &deceptive_deploy_config {
592 if deploy_config.enabled {
593 info!("Deceptive deploy mode enabled - applying production-like configuration");
594
595 if let Some(prod_cors) = &deploy_config.cors {
597 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
598 enabled: true,
599 allowed_origins: prod_cors.allowed_origins.clone(),
600 allowed_methods: prod_cors.allowed_methods.clone(),
601 allowed_headers: prod_cors.allowed_headers.clone(),
602 allow_credentials: prod_cors.allow_credentials,
603 });
604 info!("Applied production-like CORS configuration");
605 }
606
607 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
609 rate_limit_config = middleware::RateLimitConfig {
610 requests_per_minute: prod_rate_limit.requests_per_minute,
611 burst: prod_rate_limit.burst,
612 per_ip: prod_rate_limit.per_ip,
613 per_endpoint: false,
614 };
615 info!(
616 "Applied production-like rate limiting: {} req/min, burst: {}",
617 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
618 );
619 }
620
621 if !deploy_config.headers.is_empty() {
623 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
624 production_headers = Some(std::sync::Arc::new(headers_map));
625 info!("Configured {} production headers", deploy_config.headers.len());
626 }
627
628 if let Some(prod_oauth) = &deploy_config.oauth {
630 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
631 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
632 oauth2: Some(oauth2_config),
633 ..Default::default()
634 });
635 info!("Applied production-like OAuth configuration for deceptive deploy");
636 }
637 }
638 }
639
640 let rate_limiter =
641 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
642
643 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
644
645 if let Some(headers) = production_headers.clone() {
647 state = state.with_production_headers(headers);
648 }
649
650 let spec_path_for_mgmt = spec_path.clone();
652
653 if let Some(spec_path) = spec_path {
655 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
656
657 let spec_load_start = Instant::now();
659 match OpenApiSpec::from_file(&spec_path).await {
660 Ok(openapi) => {
661 let spec_load_duration = spec_load_start.elapsed();
662 info!(
663 "Successfully loaded OpenAPI spec from {} (took {:?})",
664 spec_path, spec_load_duration
665 );
666
667 tracing::debug!("Creating OpenAPI route registry...");
669 let registry_start = Instant::now();
670
671 let persona = load_persona_from_config().await;
673
674 let registry = if let Some(opts) = options {
675 tracing::debug!("Using custom validation options");
676 if let Some(ref persona) = persona {
677 tracing::info!("Using persona '{}' for route generation", persona.name);
678 }
679 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
680 } else {
681 tracing::debug!("Using environment-based options");
682 if let Some(ref persona) = persona {
683 tracing::info!("Using persona '{}' for route generation", persona.name);
684 }
685 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
686 };
687 let registry_duration = registry_start.elapsed();
688 info!(
689 "Created OpenAPI route registry with {} routes (took {:?})",
690 registry.routes().len(),
691 registry_duration
692 );
693
694 let extract_start = Instant::now();
696 let route_info: Vec<RouteInfo> = registry
697 .routes()
698 .iter()
699 .map(|route| RouteInfo {
700 method: route.method.clone(),
701 path: route.path.clone(),
702 operation_id: route.operation.operation_id.clone(),
703 summary: route.operation.summary.clone(),
704 description: route.operation.description.clone(),
705 parameters: route.parameters.clone(),
706 })
707 .collect();
708 state.routes = route_info;
709 let extract_duration = extract_start.elapsed();
710 debug!("Extracted route information (took {:?})", extract_duration);
711
712 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
714 tracing::debug!("Loading overrides from environment variable");
715 let overrides_start = Instant::now();
716 match mockforge_core::Overrides::load_from_globs(&[]).await {
717 Ok(overrides) => {
718 let overrides_duration = overrides_start.elapsed();
719 info!(
720 "Loaded {} override rules (took {:?})",
721 overrides.rules().len(),
722 overrides_duration
723 );
724 Some(overrides)
725 }
726 Err(e) => {
727 tracing::warn!("Failed to load overrides: {}", e);
728 None
729 }
730 }
731 } else {
732 None
733 };
734
735 let router_build_start = Instant::now();
737 let overrides_enabled = overrides.is_some();
738 let openapi_router = if let Some(mockai_instance) = &mockai {
739 tracing::debug!("Building router with MockAI support");
740 registry.build_router_with_mockai(Some(mockai_instance.clone()))
741 } else if let Some(ai_generator) = &ai_generator {
742 tracing::debug!("Building router with AI generator support");
743 registry.build_router_with_ai(Some(ai_generator.clone()))
744 } else if let Some(failure_config) = &failure_config {
745 tracing::debug!("Building router with failure injection and overrides");
746 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
747 registry.build_router_with_injectors_and_overrides(
748 LatencyInjector::default(),
749 Some(failure_injector),
750 overrides,
751 overrides_enabled,
752 )
753 } else {
754 tracing::debug!("Building router with overrides");
755 registry.build_router_with_injectors_and_overrides(
756 LatencyInjector::default(),
757 None,
758 overrides,
759 overrides_enabled,
760 )
761 };
762 let router_build_duration = router_build_start.elapsed();
763 debug!("Built OpenAPI router (took {:?})", router_build_duration);
764
765 tracing::debug!("Merging OpenAPI router with main router");
766 app = app.merge(openapi_router);
767 tracing::debug!("Router built successfully");
768 }
769 Err(e) => {
770 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
771 }
772 }
773 }
774
775 app = app.route(
777 "/health",
778 axum::routing::get(|| async {
779 use mockforge_core::server_utils::health::HealthStatus;
780 {
781 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
783 Ok(value) => Json(value),
784 Err(e) => {
785 tracing::error!("Failed to serialize health status: {}", e);
787 Json(serde_json::json!({
788 "status": "healthy",
789 "service": "mockforge-http",
790 "uptime_seconds": 0
791 }))
792 }
793 }
794 }
795 }),
796 )
797 .merge(sse::sse_router())
799 .merge(file_server::file_serving_router());
801
802 let state_for_routes = state.clone();
804
805 let routes_router = Router::new()
807 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
808 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
809 .with_state(state_for_routes);
810
811 app = app.merge(routes_router);
813
814 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
816
817 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
820 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
821
822 if Path::new(&coverage_html_path).exists() {
824 app = app.nest_service(
825 "/__mockforge/coverage.html",
826 tower_http::services::ServeFile::new(&coverage_html_path),
827 );
828 debug!("Serving coverage UI from: {}", coverage_html_path);
829 } else {
830 debug!(
831 "Coverage UI file not found at: {}. Skipping static file serving.",
832 coverage_html_path
833 );
834 }
835
836 let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
839 match OpenApiSpec::from_file(sp).await {
840 Ok(s) => Some(Arc::new(s)),
841 Err(e) => {
842 debug!("Failed to load OpenAPI spec for management API: {}", e);
843 None
844 }
845 }
846 } else {
847 None
848 };
849 let mgmt_port = std::env::var("PORT")
850 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
851 .ok()
852 .and_then(|p| p.parse().ok())
853 .unwrap_or(3000);
854 let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
855
856 use std::sync::Arc;
858 let ws_state = WsManagementState::new();
859 let ws_broadcast = Arc::new(ws_state.tx.clone());
860 let management_state = management_state.with_ws_broadcast(ws_broadcast);
861
862 #[cfg(feature = "smtp")]
866 let management_state = {
867 if let Some(smtp_reg) = smtp_registry {
868 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
869 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
870 Err(e) => {
871 error!(
872 "Invalid SMTP registry type passed to HTTP management state: {:?}",
873 e.type_id()
874 );
875 management_state
876 }
877 }
878 } else {
879 management_state
880 }
881 };
882 #[cfg(not(feature = "smtp"))]
883 let management_state = management_state;
884 #[cfg(not(feature = "smtp"))]
885 let _ = smtp_registry;
886 app = app.nest("/__mockforge/api", management_router(management_state));
887
888 app = app.merge(verification_router());
890
891 use crate::auth::oidc::oidc_router;
893 app = app.merge(oidc_router());
894
895 {
897 use mockforge_core::security::get_global_access_review_service;
898 if let Some(service) = get_global_access_review_service().await {
899 use crate::handlers::access_review::{access_review_router, AccessReviewState};
900 let review_state = AccessReviewState { service };
901 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
902 debug!("Access review API mounted at /api/v1/security/access-reviews");
903 }
904 }
905
906 {
908 use mockforge_core::security::get_global_privileged_access_manager;
909 if let Some(manager) = get_global_privileged_access_manager().await {
910 use crate::handlers::privileged_access::{
911 privileged_access_router, PrivilegedAccessState,
912 };
913 let privileged_state = PrivilegedAccessState { manager };
914 app = app.nest(
915 "/api/v1/security/privileged-access",
916 privileged_access_router(privileged_state),
917 );
918 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
919 }
920 }
921
922 {
924 use mockforge_core::security::get_global_change_management_engine;
925 if let Some(engine) = get_global_change_management_engine().await {
926 use crate::handlers::change_management::{
927 change_management_router, ChangeManagementState,
928 };
929 let change_state = ChangeManagementState { engine };
930 app = app.nest("/api/v1/change-management", change_management_router(change_state));
931 debug!("Change management API mounted at /api/v1/change-management");
932 }
933 }
934
935 {
937 use mockforge_core::security::get_global_risk_assessment_engine;
938 if let Some(engine) = get_global_risk_assessment_engine().await {
939 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
940 let risk_state = RiskAssessmentState { engine };
941 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
942 debug!("Risk assessment API mounted at /api/v1/security/risks");
943 }
944 }
945
946 {
948 use crate::auth::token_lifecycle::TokenLifecycleManager;
949 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
950 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
951 let lifecycle_state = TokenLifecycleState {
952 manager: lifecycle_manager,
953 };
954 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
955 debug!("Token lifecycle API mounted at /api/v1/auth");
956 }
957
958 {
960 use crate::auth::oidc::load_oidc_state;
961 use crate::auth::token_lifecycle::TokenLifecycleManager;
962 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
963 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
965 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
966 let oauth2_state = OAuth2ServerState {
967 oidc_state,
968 lifecycle_manager,
969 auth_codes: Arc::new(RwLock::new(HashMap::new())),
970 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
971 };
972 app = app.merge(oauth2_server_router(oauth2_state));
973 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
974 }
975
976 {
978 use crate::auth::oidc::load_oidc_state;
979 use crate::auth::risk_engine::RiskEngine;
980 use crate::auth::token_lifecycle::TokenLifecycleManager;
981 use crate::handlers::consent::{consent_router, ConsentState};
982 use crate::handlers::oauth2_server::OAuth2ServerState;
983 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
985 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
986 let oauth2_state = OAuth2ServerState {
987 oidc_state: oidc_state.clone(),
988 lifecycle_manager: lifecycle_manager.clone(),
989 auth_codes: Arc::new(RwLock::new(HashMap::new())),
990 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
991 };
992 let risk_engine = Arc::new(RiskEngine::default());
993 let consent_state = ConsentState {
994 oauth2_state,
995 risk_engine,
996 };
997 app = app.merge(consent_router(consent_state));
998 debug!("Consent screen endpoints mounted at /consent");
999 }
1000
1001 {
1003 use crate::auth::risk_engine::RiskEngine;
1004 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1005 let risk_engine = Arc::new(RiskEngine::default());
1006 let risk_state = RiskSimulationState { risk_engine };
1007 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1008 debug!("Risk simulation API mounted at /api/v1/auth/risk");
1009 }
1010
1011 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1013
1014 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1016
1017 app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1019
1020 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1023
1024 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1026
1027 if state.production_headers.is_some() {
1029 app =
1030 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1031 }
1032
1033 if let Some(auth_config) = deceptive_deploy_auth_config {
1035 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1036 use std::collections::HashMap;
1037 use std::sync::Arc;
1038 use tokio::sync::RwLock;
1039
1040 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1042 match create_oauth2_client(oauth2_config) {
1043 Ok(client) => Some(client),
1044 Err(e) => {
1045 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1046 None
1047 }
1048 }
1049 } else {
1050 None
1051 };
1052
1053 let auth_state = AuthState {
1055 config: auth_config,
1056 spec: None, oauth2_client,
1058 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1059 };
1060
1061 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1063 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1064 }
1065
1066 app = apply_cors_middleware(app, final_cors_config);
1068
1069 if let Some(mt_config) = multi_tenant_config {
1071 if mt_config.enabled {
1072 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1073 use std::sync::Arc;
1074
1075 info!(
1076 "Multi-tenant mode enabled with {} routing strategy",
1077 match mt_config.routing_strategy {
1078 mockforge_core::RoutingStrategy::Path => "path-based",
1079 mockforge_core::RoutingStrategy::Port => "port-based",
1080 mockforge_core::RoutingStrategy::Both => "hybrid",
1081 }
1082 );
1083
1084 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1086
1087 let default_workspace =
1089 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1090 if let Err(e) =
1091 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1092 {
1093 warn!("Failed to register default workspace: {}", e);
1094 } else {
1095 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1096 }
1097
1098 if mt_config.auto_discover {
1100 if let Some(config_dir) = &mt_config.config_directory {
1101 let config_path = Path::new(config_dir);
1102 if config_path.exists() && config_path.is_dir() {
1103 match fs::read_dir(config_path).await {
1104 Ok(mut entries) => {
1105 while let Ok(Some(entry)) = entries.next_entry().await {
1106 let path = entry.path();
1107 if path.extension() == Some(OsStr::new("yaml")) {
1108 match fs::read_to_string(&path).await {
1109 Ok(content) => {
1110 match serde_yaml::from_str::<
1111 mockforge_core::Workspace,
1112 >(
1113 &content
1114 ) {
1115 Ok(workspace) => {
1116 if let Err(e) = registry.register_workspace(
1117 workspace.id.clone(),
1118 workspace,
1119 ) {
1120 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1121 } else {
1122 info!("Auto-registered workspace from {:?}", path);
1123 }
1124 }
1125 Err(e) => {
1126 warn!("Failed to parse workspace from {:?}: {}", path, e);
1127 }
1128 }
1129 }
1130 Err(e) => {
1131 warn!(
1132 "Failed to read workspace file {:?}: {}",
1133 path, e
1134 );
1135 }
1136 }
1137 }
1138 }
1139 }
1140 Err(e) => {
1141 warn!("Failed to read config directory {:?}: {}", config_path, e);
1142 }
1143 }
1144 } else {
1145 warn!(
1146 "Config directory {:?} does not exist or is not a directory",
1147 config_path
1148 );
1149 }
1150 }
1151 }
1152
1153 let registry = Arc::new(registry);
1155
1156 let _workspace_router = WorkspaceRouter::new(registry);
1158
1159 info!("Workspace routing middleware initialized for HTTP server");
1162 }
1163 }
1164
1165 let total_startup_duration = startup_start.elapsed();
1166 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1167
1168 app
1169}
1170
1171pub async fn build_router_with_auth_and_latency(
1173 spec_path: Option<String>,
1174 _options: Option<()>,
1175 auth_config: Option<mockforge_core::config::AuthConfig>,
1176 latency_injector: Option<LatencyInjector>,
1177) -> Router {
1178 let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1180
1181 if let Some(injector) = latency_injector {
1183 let injector = Arc::new(injector);
1184 app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1185 let injector = injector.clone();
1186 async move {
1187 let _ = injector.inject_latency(&[]).await;
1188 next.run(req).await
1189 }
1190 }));
1191 }
1192
1193 app
1194}
1195
1196pub async fn build_router_with_latency(
1198 spec_path: Option<String>,
1199 options: Option<ValidationOptions>,
1200 latency_injector: Option<LatencyInjector>,
1201) -> Router {
1202 if let Some(spec) = &spec_path {
1203 match OpenApiSpec::from_file(spec).await {
1204 Ok(openapi) => {
1205 let registry = if let Some(opts) = options {
1206 OpenApiRouteRegistry::new_with_options(openapi, opts)
1207 } else {
1208 OpenApiRouteRegistry::new_with_env(openapi)
1209 };
1210
1211 if let Some(injector) = latency_injector {
1212 return registry.build_router_with_latency(injector);
1213 } else {
1214 return registry.build_router();
1215 }
1216 }
1217 Err(e) => {
1218 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1219 }
1220 }
1221 }
1222
1223 build_router(None, None, None).await
1224}
1225
1226pub async fn build_router_with_auth(
1228 spec_path: Option<String>,
1229 options: Option<ValidationOptions>,
1230 auth_config: Option<mockforge_core::config::AuthConfig>,
1231) -> Router {
1232 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1233 use std::sync::Arc;
1234
1235 #[cfg(feature = "data-faker")]
1237 {
1238 register_core_faker_provider();
1239 }
1240
1241 let spec = if let Some(spec_path) = &spec_path {
1243 match OpenApiSpec::from_file(&spec_path).await {
1244 Ok(spec) => Some(Arc::new(spec)),
1245 Err(e) => {
1246 warn!("Failed to load OpenAPI spec for auth: {}", e);
1247 None
1248 }
1249 }
1250 } else {
1251 None
1252 };
1253
1254 let oauth2_client = if let Some(auth_config) = &auth_config {
1256 if let Some(oauth2_config) = &auth_config.oauth2 {
1257 match create_oauth2_client(oauth2_config) {
1258 Ok(client) => Some(client),
1259 Err(e) => {
1260 warn!("Failed to create OAuth2 client: {}", e);
1261 None
1262 }
1263 }
1264 } else {
1265 None
1266 }
1267 } else {
1268 None
1269 };
1270
1271 let auth_state = AuthState {
1272 config: auth_config.unwrap_or_default(),
1273 spec,
1274 oauth2_client,
1275 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1276 };
1277
1278 let mut app = Router::new().with_state(auth_state.clone());
1280
1281 if let Some(spec_path) = spec_path {
1283 match OpenApiSpec::from_file(&spec_path).await {
1284 Ok(openapi) => {
1285 info!("Loaded OpenAPI spec from {}", spec_path);
1286 let registry = if let Some(opts) = options {
1287 OpenApiRouteRegistry::new_with_options(openapi, opts)
1288 } else {
1289 OpenApiRouteRegistry::new_with_env(openapi)
1290 };
1291
1292 app = registry.build_router();
1293 }
1294 Err(e) => {
1295 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1296 }
1297 }
1298 }
1299
1300 app = app.route(
1302 "/health",
1303 axum::routing::get(|| async {
1304 use mockforge_core::server_utils::health::HealthStatus;
1305 {
1306 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1308 Ok(value) => Json(value),
1309 Err(e) => {
1310 tracing::error!("Failed to serialize health status: {}", e);
1312 Json(serde_json::json!({
1313 "status": "healthy",
1314 "service": "mockforge-http",
1315 "uptime_seconds": 0
1316 }))
1317 }
1318 }
1319 }
1320 }),
1321 )
1322 .merge(sse::sse_router())
1324 .merge(file_server::file_serving_router())
1326 .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1328 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1330
1331 app
1332}
1333
1334pub async fn serve_router(
1336 port: u16,
1337 app: Router,
1338) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1339 serve_router_with_tls(port, app, None).await
1340}
1341
1342pub async fn serve_router_with_tls(
1344 port: u16,
1345 app: Router,
1346 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1347) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1348 use std::net::SocketAddr;
1349
1350 let addr = mockforge_core::wildcard_socket_addr(port);
1351
1352 if let Some(ref tls) = tls_config {
1353 if tls.enabled {
1354 info!("HTTPS listening on {}", addr);
1355 return serve_with_tls(addr, app, tls).await;
1356 }
1357 }
1358
1359 info!("HTTP listening on {}", addr);
1360
1361 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1362 format!(
1363 "Failed to bind HTTP server to port {}: {}\n\
1364 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 {}",
1365 port, e, port, port
1366 )
1367 })?;
1368
1369 let odata_app = tower::ServiceBuilder::new()
1373 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1374 .service(app);
1375 let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
1376 axum::serve(listener, make_svc).await?;
1377 Ok(())
1378}
1379
1380async fn serve_with_tls(
1385 addr: std::net::SocketAddr,
1386 app: Router,
1387 tls_config: &mockforge_core::config::HttpTlsConfig,
1388) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1389 use axum_server::tls_rustls::RustlsConfig;
1390 use std::net::SocketAddr;
1391
1392 tls::init_crypto_provider();
1394
1395 info!("Loading TLS configuration for HTTPS server");
1396
1397 let server_config = tls::load_tls_server_config(tls_config)?;
1399
1400 let rustls_config = RustlsConfig::from_config(server_config);
1403
1404 info!("Starting HTTPS server on {}", addr);
1405
1406 let odata_app = tower::ServiceBuilder::new()
1410 .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1411 .service(app);
1412 let make_svc = axum::ServiceExt::<axum::http::Request<axum::body::Body>>::into_make_service_with_connect_info::<SocketAddr>(odata_app);
1413
1414 axum_server::bind_rustls(addr, rustls_config)
1416 .serve(make_svc)
1417 .await
1418 .map_err(|e| format!("HTTPS server error: {}", e).into())
1419}
1420
1421pub async fn start(
1423 port: u16,
1424 spec_path: Option<String>,
1425 options: Option<ValidationOptions>,
1426) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1427 start_with_latency(port, spec_path, options, None).await
1428}
1429
1430pub async fn start_with_auth_and_latency(
1432 port: u16,
1433 spec_path: Option<String>,
1434 options: Option<ValidationOptions>,
1435 auth_config: Option<mockforge_core::config::AuthConfig>,
1436 latency_profile: Option<LatencyProfile>,
1437) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1438 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1439 .await
1440}
1441
1442pub async fn start_with_auth_and_injectors(
1444 port: u16,
1445 spec_path: Option<String>,
1446 options: Option<ValidationOptions>,
1447 auth_config: Option<mockforge_core::config::AuthConfig>,
1448 _latency_profile: Option<LatencyProfile>,
1449 _failure_injector: Option<FailureInjector>,
1450) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1451 let app = build_router_with_auth(spec_path, options, auth_config).await;
1453 serve_router(port, app).await
1454}
1455
1456pub async fn start_with_latency(
1458 port: u16,
1459 spec_path: Option<String>,
1460 options: Option<ValidationOptions>,
1461 latency_profile: Option<LatencyProfile>,
1462) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1463 let latency_injector =
1464 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1465
1466 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1467 serve_router(port, app).await
1468}
1469
1470pub async fn build_router_with_chains(
1472 spec_path: Option<String>,
1473 options: Option<ValidationOptions>,
1474 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1475) -> Router {
1476 build_router_with_chains_and_multi_tenant(
1477 spec_path,
1478 options,
1479 circling_config,
1480 None,
1481 None,
1482 None,
1483 None,
1484 None,
1485 None,
1486 None,
1487 false,
1488 None, None, None, None, )
1493 .await
1494}
1495
1496async fn apply_route_chaos(
1504 injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1505 method: &http::Method,
1506 uri: &http::Uri,
1507) -> Option<axum::response::Response> {
1508 use axum::http::StatusCode;
1509 use axum::response::IntoResponse;
1510
1511 if let Some(injector) = injector {
1512 if let Some(fault_response) = injector.get_fault_response(method, uri) {
1514 let mut response = Json(serde_json::json!({
1516 "error": fault_response.error_message,
1517 "fault_type": fault_response.fault_type,
1518 }))
1519 .into_response();
1520 *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1521 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1522 return Some(response);
1523 }
1524
1525 if let Err(e) = injector.inject_latency(method, uri).await {
1527 tracing::warn!("Failed to inject latency: {}", e);
1528 }
1529 }
1530
1531 None }
1533
1534#[allow(clippy::too_many_arguments)]
1536pub async fn build_router_with_chains_and_multi_tenant(
1537 spec_path: Option<String>,
1538 options: Option<ValidationOptions>,
1539 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1540 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
1541 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1542 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1543 _ai_generator: Option<Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>>,
1544 smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1545 mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1546 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1547 traffic_shaping_enabled: bool,
1548 health_manager: Option<Arc<HealthManager>>,
1549 _mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1550 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1551 proxy_config: Option<mockforge_core::proxy::config::ProxyConfig>,
1552) -> Router {
1553 use crate::latency_profiles::LatencyProfiles;
1554 use crate::op_middleware::Shared;
1555 use mockforge_core::Overrides;
1556
1557 let template_expand =
1559 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1560 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1561 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1562 .unwrap_or(false)
1563 });
1564
1565 let _shared = Shared {
1566 profiles: LatencyProfiles::default(),
1567 overrides: Overrides::default(),
1568 failure_injector: None,
1569 traffic_shaper,
1570 overrides_enabled: false,
1571 traffic_shaping_enabled,
1572 };
1573
1574 let mut app = Router::new();
1576 let mut include_default_health = true;
1577 let mut captured_routes: Vec<RouteInfo> = Vec::new();
1578
1579 if let Some(ref spec) = spec_path {
1581 match OpenApiSpec::from_file(&spec).await {
1582 Ok(openapi) => {
1583 info!("Loaded OpenAPI spec from {}", spec);
1584
1585 let persona = load_persona_from_config().await;
1587
1588 let mut registry = if let Some(opts) = options {
1589 tracing::debug!("Using custom validation options");
1590 if let Some(ref persona) = persona {
1591 tracing::info!("Using persona '{}' for route generation", persona.name);
1592 }
1593 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1594 } else {
1595 tracing::debug!("Using environment-based options");
1596 if let Some(ref persona) = persona {
1597 tracing::info!("Using persona '{}' for route generation", persona.name);
1598 }
1599 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1600 };
1601
1602 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1604 .unwrap_or_else(|_| "/app/fixtures".to_string());
1605 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1606 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1607 .unwrap_or(true); if custom_fixtures_enabled {
1610 use mockforge_core::CustomFixtureLoader;
1611 use std::path::PathBuf;
1612 use std::sync::Arc;
1613
1614 let fixtures_path = PathBuf::from(&fixtures_dir);
1615 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1616
1617 if let Err(e) = custom_loader.load_fixtures().await {
1618 tracing::warn!("Failed to load custom fixtures: {}", e);
1619 } else {
1620 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1621 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1622 }
1623 }
1624
1625 if registry
1626 .routes()
1627 .iter()
1628 .any(|route| route.method == "GET" && route.path == "/health")
1629 {
1630 include_default_health = false;
1631 }
1632 captured_routes = registry
1634 .routes()
1635 .iter()
1636 .map(|r| RouteInfo {
1637 method: r.method.clone(),
1638 path: r.path.clone(),
1639 operation_id: r.operation.operation_id.clone(),
1640 summary: r.operation.summary.clone(),
1641 description: r.operation.description.clone(),
1642 parameters: r.parameters.clone(),
1643 })
1644 .collect();
1645
1646 {
1649 let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1650 captured_routes
1651 .iter()
1652 .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1653 method: r.method.clone(),
1654 path: r.path.clone(),
1655 operation_id: r.operation_id.clone(),
1656 summary: r.summary.clone(),
1657 description: r.description.clone(),
1658 parameters: r.parameters.clone(),
1659 })
1660 .collect();
1661 mockforge_core::request_logger::set_global_routes(global_routes);
1662 tracing::info!("Stored {} routes in global route store", captured_routes.len());
1663 }
1664
1665 let spec_router = if let Some(ref mockai_instance) = _mockai {
1667 tracing::debug!("Building router with MockAI support");
1668 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1669 } else {
1670 registry.build_router()
1671 };
1672 app = app.merge(spec_router);
1673 }
1674 Err(e) => {
1675 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1676 }
1677 }
1678 }
1679
1680 let route_chaos_injector: Option<
1684 std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1685 > = if let Some(ref route_configs) = route_configs {
1686 if !route_configs.is_empty() {
1687 let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1690 route_configs.to_vec();
1691 match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1692 Ok(injector) => {
1693 info!(
1694 "Initialized advanced routing features for {} route(s)",
1695 route_configs.len()
1696 );
1697 Some(std::sync::Arc::new(injector)
1700 as std::sync::Arc<
1701 dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1702 >)
1703 }
1704 Err(e) => {
1705 warn!(
1706 "Failed to initialize advanced routing features: {}. Using basic routing.",
1707 e
1708 );
1709 None
1710 }
1711 }
1712 } else {
1713 None
1714 }
1715 } else {
1716 None
1717 };
1718
1719 if let Some(route_configs) = route_configs {
1720 use axum::http::StatusCode;
1721 use axum::response::IntoResponse;
1722
1723 if !route_configs.is_empty() {
1724 info!("Registering {} custom route(s) from config", route_configs.len());
1725 }
1726
1727 let injector = route_chaos_injector.clone();
1728 for route_config in route_configs {
1729 let status = route_config.response.status;
1730 let body = route_config.response.body.clone();
1731 let headers = route_config.response.headers.clone();
1732 let path = route_config.path.clone();
1733 let method = route_config.method.clone();
1734
1735 let expected_method = method.to_uppercase();
1740 let injector_clone = injector.clone();
1744 app = app.route(
1745 &path,
1746 #[allow(clippy::non_send_fields_in_send_ty)]
1747 axum::routing::any(move |req: http::Request<axum::body::Body>| {
1748 let body = body.clone();
1749 let headers = headers.clone();
1750 let expand = template_expand;
1751 let expected = expected_method.clone();
1752 let status_code = status;
1753 let injector_for_chaos = injector_clone.clone();
1755
1756 async move {
1757 if req.method().as_str() != expected.as_str() {
1759 return axum::response::Response::builder()
1761 .status(StatusCode::METHOD_NOT_ALLOWED)
1762 .header("Allow", &expected)
1763 .body(axum::body::Body::empty())
1764 .unwrap()
1765 .into_response();
1766 }
1767
1768 if let Some(fault_response) = apply_route_chaos(
1772 injector_for_chaos.as_deref(),
1773 req.method(),
1774 req.uri(),
1775 )
1776 .await
1777 {
1778 return fault_response;
1779 }
1780
1781 let mut body_value = body.unwrap_or(serde_json::json!({}));
1783
1784 if expand {
1788 use mockforge_template_expansion::RequestContext;
1789 use serde_json::Value;
1790 use std::collections::HashMap;
1791
1792 let method = req.method().to_string();
1794 let path = req.uri().path().to_string();
1795
1796 let query_params: HashMap<String, Value> = req
1798 .uri()
1799 .query()
1800 .map(|q| {
1801 url::form_urlencoded::parse(q.as_bytes())
1802 .into_owned()
1803 .map(|(k, v)| (k, Value::String(v)))
1804 .collect()
1805 })
1806 .unwrap_or_default();
1807
1808 let headers: HashMap<String, Value> = req
1810 .headers()
1811 .iter()
1812 .map(|(k, v)| {
1813 (
1814 k.to_string(),
1815 Value::String(v.to_str().unwrap_or_default().to_string()),
1816 )
1817 })
1818 .collect();
1819
1820 let context = RequestContext {
1824 method,
1825 path,
1826 query_params,
1827 headers,
1828 body: None, path_params: HashMap::new(),
1830 multipart_fields: HashMap::new(),
1831 multipart_files: HashMap::new(),
1832 };
1833
1834 let body_value_clone = body_value.clone();
1838 let context_clone = context.clone();
1839 body_value = match tokio::task::spawn_blocking(move || {
1840 mockforge_template_expansion::expand_templates_in_json(
1841 body_value_clone,
1842 &context_clone,
1843 )
1844 })
1845 .await
1846 {
1847 Ok(result) => result,
1848 Err(_) => body_value, };
1850 }
1851
1852 let mut response = Json(body_value).into_response();
1853
1854 *response.status_mut() =
1856 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
1857
1858 for (key, value) in headers {
1860 if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
1861 if let Ok(header_value) = http::HeaderValue::from_str(&value) {
1862 response.headers_mut().insert(header_name, header_value);
1863 }
1864 }
1865 }
1866
1867 response
1868 }
1869 }),
1870 );
1871
1872 debug!("Registered route: {} {}", method, path);
1873 }
1874 }
1875
1876 if let Some(health) = health_manager {
1878 app = app.merge(health::health_router(health));
1880 info!(
1881 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1882 );
1883 } else if include_default_health {
1884 app = app.route(
1886 "/health",
1887 axum::routing::get(|| async {
1888 use mockforge_core::server_utils::health::HealthStatus;
1889 {
1890 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1892 Ok(value) => Json(value),
1893 Err(e) => {
1894 tracing::error!("Failed to serialize health status: {}", e);
1896 Json(serde_json::json!({
1897 "status": "healthy",
1898 "service": "mockforge-http",
1899 "uptime_seconds": 0
1900 }))
1901 }
1902 }
1903 }
1904 }),
1905 );
1906 }
1907
1908 app = app.merge(sse::sse_router());
1909 app = app.merge(file_server::file_serving_router());
1911
1912 let mgmt_spec = if let Some(ref sp) = spec_path {
1915 match OpenApiSpec::from_file(sp).await {
1916 Ok(s) => Some(Arc::new(s)),
1917 Err(e) => {
1918 debug!("Failed to load OpenAPI spec for management API: {}", e);
1919 None
1920 }
1921 }
1922 } else {
1923 None
1924 };
1925 let spec_path_clone = spec_path.clone();
1926 let mgmt_port = std::env::var("PORT")
1927 .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
1928 .ok()
1929 .and_then(|p| p.parse().ok())
1930 .unwrap_or(3000);
1931 let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
1932
1933 use std::sync::Arc;
1935 let ws_state = WsManagementState::new();
1936 let ws_broadcast = Arc::new(ws_state.tx.clone());
1937 let management_state = management_state.with_ws_broadcast(ws_broadcast);
1938
1939 let management_state = if let Some(proxy_cfg) = proxy_config {
1941 use tokio::sync::RwLock;
1942 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
1943 management_state.with_proxy_config(proxy_config_arc)
1944 } else {
1945 management_state
1946 };
1947
1948 #[cfg(feature = "smtp")]
1949 let management_state = {
1950 if let Some(smtp_reg) = smtp_registry {
1951 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
1952 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
1953 Err(e) => {
1954 error!(
1955 "Invalid SMTP registry type passed to HTTP management state: {:?}",
1956 e.type_id()
1957 );
1958 management_state
1959 }
1960 }
1961 } else {
1962 management_state
1963 }
1964 };
1965 #[cfg(not(feature = "smtp"))]
1966 let management_state = {
1967 let _ = smtp_registry;
1968 management_state
1969 };
1970 #[cfg(feature = "mqtt")]
1971 let management_state = {
1972 if let Some(broker) = mqtt_broker {
1973 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
1974 Ok(broker) => management_state.with_mqtt_broker(broker),
1975 Err(e) => {
1976 error!(
1977 "Invalid MQTT broker passed to HTTP management state: {:?}",
1978 e.type_id()
1979 );
1980 management_state
1981 }
1982 }
1983 } else {
1984 management_state
1985 }
1986 };
1987 #[cfg(not(feature = "mqtt"))]
1988 let management_state = {
1989 let _ = mqtt_broker;
1990 management_state
1991 };
1992 app = app.nest("/__mockforge/api", management_router(management_state));
1993
1994 app = app.merge(verification_router());
1996
1997 use crate::auth::oidc::oidc_router;
1999 app = app.merge(oidc_router());
2000
2001 {
2003 use mockforge_core::security::get_global_access_review_service;
2004 if let Some(service) = get_global_access_review_service().await {
2005 use crate::handlers::access_review::{access_review_router, AccessReviewState};
2006 let review_state = AccessReviewState { service };
2007 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2008 debug!("Access review API mounted at /api/v1/security/access-reviews");
2009 }
2010 }
2011
2012 {
2014 use mockforge_core::security::get_global_privileged_access_manager;
2015 if let Some(manager) = get_global_privileged_access_manager().await {
2016 use crate::handlers::privileged_access::{
2017 privileged_access_router, PrivilegedAccessState,
2018 };
2019 let privileged_state = PrivilegedAccessState { manager };
2020 app = app.nest(
2021 "/api/v1/security/privileged-access",
2022 privileged_access_router(privileged_state),
2023 );
2024 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2025 }
2026 }
2027
2028 {
2030 use mockforge_core::security::get_global_change_management_engine;
2031 if let Some(engine) = get_global_change_management_engine().await {
2032 use crate::handlers::change_management::{
2033 change_management_router, ChangeManagementState,
2034 };
2035 let change_state = ChangeManagementState { engine };
2036 app = app.nest("/api/v1/change-management", change_management_router(change_state));
2037 debug!("Change management API mounted at /api/v1/change-management");
2038 }
2039 }
2040
2041 {
2043 use mockforge_core::security::get_global_risk_assessment_engine;
2044 if let Some(engine) = get_global_risk_assessment_engine().await {
2045 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2046 let risk_state = RiskAssessmentState { engine };
2047 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2048 debug!("Risk assessment API mounted at /api/v1/security/risks");
2049 }
2050 }
2051
2052 {
2054 use crate::auth::token_lifecycle::TokenLifecycleManager;
2055 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2056 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2057 let lifecycle_state = TokenLifecycleState {
2058 manager: lifecycle_manager,
2059 };
2060 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2061 debug!("Token lifecycle API mounted at /api/v1/auth");
2062 }
2063
2064 {
2066 use crate::auth::oidc::load_oidc_state;
2067 use crate::auth::token_lifecycle::TokenLifecycleManager;
2068 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2069 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2071 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2072 let oauth2_state = OAuth2ServerState {
2073 oidc_state,
2074 lifecycle_manager,
2075 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2076 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2077 };
2078 app = app.merge(oauth2_server_router(oauth2_state));
2079 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2080 }
2081
2082 {
2084 use crate::auth::oidc::load_oidc_state;
2085 use crate::auth::risk_engine::RiskEngine;
2086 use crate::auth::token_lifecycle::TokenLifecycleManager;
2087 use crate::handlers::consent::{consent_router, ConsentState};
2088 use crate::handlers::oauth2_server::OAuth2ServerState;
2089 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2091 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2092 let oauth2_state = OAuth2ServerState {
2093 oidc_state: oidc_state.clone(),
2094 lifecycle_manager: lifecycle_manager.clone(),
2095 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2096 refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2097 };
2098 let risk_engine = Arc::new(RiskEngine::default());
2099 let consent_state = ConsentState {
2100 oauth2_state,
2101 risk_engine,
2102 };
2103 app = app.merge(consent_router(consent_state));
2104 debug!("Consent screen endpoints mounted at /consent");
2105 }
2106
2107 {
2109 use crate::auth::risk_engine::RiskEngine;
2110 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2111 let risk_engine = Arc::new(RiskEngine::default());
2112 let risk_state = RiskSimulationState { risk_engine };
2113 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2114 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2115 }
2116
2117 let database = {
2119 use crate::database::Database;
2120 let database_url = std::env::var("DATABASE_URL").ok();
2121 match Database::connect_optional(database_url.as_deref()).await {
2122 Ok(db) => {
2123 if db.is_connected() {
2124 if let Err(e) = db.migrate_if_connected().await {
2126 warn!("Failed to run database migrations: {}", e);
2127 } else {
2128 info!("Database connected and migrations applied");
2129 }
2130 }
2131 Some(db)
2132 }
2133 Err(e) => {
2134 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2135 None
2136 }
2137 }
2138 };
2139
2140 let (drift_engine, incident_manager, drift_config) = {
2143 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2144 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2145 use std::sync::Arc;
2146
2147 let drift_config = DriftBudgetConfig::default();
2149 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2150
2151 let incident_store = Arc::new(IncidentStore::default());
2153 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2154
2155 (drift_engine, incident_manager, drift_config)
2156 };
2157
2158 {
2159 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2160 use crate::middleware::drift_tracking::DriftTrackingState;
2161 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2162 use mockforge_core::consumer_contracts::{ConsumerBreakingChangeDetector, UsageRecorder};
2163 use std::sync::Arc;
2164
2165 let usage_recorder = Arc::new(UsageRecorder::default());
2167 let consumer_detector =
2168 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2169
2170 let diff_analyzer = if drift_config.enabled {
2172 match ContractDiffAnalyzer::new(
2173 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2174 ) {
2175 Ok(analyzer) => Some(Arc::new(analyzer)),
2176 Err(e) => {
2177 warn!("Failed to create contract diff analyzer: {}", e);
2178 None
2179 }
2180 }
2181 } else {
2182 None
2183 };
2184
2185 let spec = if let Some(ref spec_path) = spec_path {
2188 match OpenApiSpec::from_file(spec_path).await {
2189 Ok(s) => Some(Arc::new(s)),
2190 Err(e) => {
2191 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2192 None
2193 }
2194 }
2195 } else {
2196 None
2197 };
2198
2199 let drift_tracking_state = DriftTrackingState {
2201 diff_analyzer,
2202 spec,
2203 drift_engine: drift_engine.clone(),
2204 incident_manager: incident_manager.clone(),
2205 usage_recorder,
2206 consumer_detector,
2207 enabled: drift_config.enabled,
2208 };
2209
2210 app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2212
2213 let drift_tracking_state_clone = drift_tracking_state.clone();
2216 app = app.layer(axum::middleware::from_fn(
2217 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2218 let state = drift_tracking_state_clone.clone();
2219 async move {
2220 if req.extensions().get::<DriftTrackingState>().is_none() {
2222 req.extensions_mut().insert(state);
2223 }
2224 middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2226 .await
2227 }
2228 },
2229 ));
2230
2231 let drift_state = DriftBudgetState {
2232 engine: drift_engine.clone(),
2233 incident_manager: incident_manager.clone(),
2234 gitops_handler: None, };
2236
2237 app = app.merge(drift_budget_router(drift_state));
2238 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2239 }
2240
2241 #[cfg(feature = "pipelines")]
2243 {
2244 use crate::handlers::pipelines::{pipeline_router, PipelineState};
2245
2246 let pipeline_state = PipelineState::new();
2247 app = app.merge(pipeline_router(pipeline_state));
2248 debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2249 }
2250
2251 {
2253 use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2254 use crate::handlers::forecasting::{forecasting_router, ForecastingState};
2255 use crate::handlers::semantic_drift::{semantic_drift_router, SemanticDriftState};
2256 use crate::handlers::threat_modeling::{threat_modeling_router, ThreatModelingState};
2257 use mockforge_core::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2258 use mockforge_core::contract_drift::threat_modeling::{
2259 ThreatAnalyzer, ThreatModelingConfig,
2260 };
2261 use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2262 use std::sync::Arc;
2263
2264 let forecasting_config = ForecastingConfig::default();
2266 let forecaster = Arc::new(Forecaster::new(forecasting_config));
2267 let forecasting_state = ForecastingState {
2268 forecaster,
2269 database: database.clone(),
2270 };
2271
2272 let semantic_manager = Arc::new(SemanticIncidentManager::new());
2274 let semantic_state = SemanticDriftState {
2275 manager: semantic_manager,
2276 database: database.clone(),
2277 };
2278
2279 let threat_config = ThreatModelingConfig::default();
2281 let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2282 Ok(analyzer) => Arc::new(analyzer),
2283 Err(e) => {
2284 warn!("Failed to create threat analyzer: {}. Using default.", e);
2285 Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2286 |_| {
2287 ThreatAnalyzer::new(ThreatModelingConfig {
2289 enabled: false,
2290 ..Default::default()
2291 })
2292 .expect("Failed to create fallback threat analyzer")
2293 },
2294 ))
2295 }
2296 };
2297 let mut webhook_configs = Vec::new();
2299 let config_paths = [
2300 "config.yaml",
2301 "mockforge.yaml",
2302 "tools/mockforge/config.yaml",
2303 "../tools/mockforge/config.yaml",
2304 ];
2305
2306 for path in &config_paths {
2307 if let Ok(config) = mockforge_core::config::load_config(path).await {
2308 if !config.incidents.webhooks.is_empty() {
2309 webhook_configs = config.incidents.webhooks.clone();
2310 info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2311 break;
2312 }
2313 }
2314 }
2315
2316 if webhook_configs.is_empty() {
2317 debug!("No webhook configs found in config files, using empty list");
2318 }
2319
2320 let threat_state = ThreatModelingState {
2321 analyzer: threat_analyzer,
2322 webhook_configs,
2323 database: database.clone(),
2324 };
2325
2326 let contract_health_state = ContractHealthState {
2328 incident_manager: incident_manager.clone(),
2329 semantic_manager: Arc::new(SemanticIncidentManager::new()),
2330 database: database.clone(),
2331 };
2332
2333 app = app.merge(forecasting_router(forecasting_state));
2335 debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2336
2337 app = app.merge(semantic_drift_router(semantic_state));
2338 debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2339
2340 app = app.merge(threat_modeling_router(threat_state));
2341 debug!("Threat modeling endpoints mounted at /api/v1/threats");
2342
2343 app = app.merge(contract_health_router(contract_health_state));
2344 debug!("Contract health endpoints mounted at /api/v1/contract-health");
2345 }
2346
2347 {
2349 use crate::handlers::protocol_contracts::{
2350 protocol_contracts_router, ProtocolContractState,
2351 };
2352 use mockforge_core::contract_drift::{
2353 ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2354 };
2355 use std::sync::Arc;
2356 use tokio::sync::RwLock;
2357
2358 let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2360
2361 let mut fitness_registry = FitnessFunctionRegistry::new();
2363
2364 let config_paths = [
2366 "config.yaml",
2367 "mockforge.yaml",
2368 "tools/mockforge/config.yaml",
2369 "../tools/mockforge/config.yaml",
2370 ];
2371
2372 let mut config_loaded = false;
2373 for path in &config_paths {
2374 if let Ok(config) = mockforge_core::config::load_config(path).await {
2375 if !config.contracts.fitness_rules.is_empty() {
2376 if let Err(e) =
2377 fitness_registry.load_from_config(&config.contracts.fitness_rules)
2378 {
2379 warn!("Failed to load fitness rules from config {}: {}", path, e);
2380 } else {
2381 info!(
2382 "Loaded {} fitness rules from config: {}",
2383 config.contracts.fitness_rules.len(),
2384 path
2385 );
2386 config_loaded = true;
2387 break;
2388 }
2389 }
2390 }
2391 }
2392
2393 if !config_loaded {
2394 debug!("No fitness rules found in config files, using empty registry");
2395 }
2396
2397 let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2398
2399 let consumer_mapping_registry =
2403 mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2404 let consumer_analyzer =
2405 Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2406
2407 let protocol_state = ProtocolContractState {
2408 registry: contract_registry,
2409 drift_engine: Some(drift_engine.clone()),
2410 incident_manager: Some(incident_manager.clone()),
2411 fitness_registry: Some(fitness_registry),
2412 consumer_analyzer: Some(consumer_analyzer),
2413 };
2414
2415 app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2416 debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2417 }
2418
2419 #[cfg(feature = "behavioral-cloning")]
2421 {
2422 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2423 use std::path::PathBuf;
2424
2425 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2427 .ok()
2428 .map(PathBuf::from)
2429 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2430
2431 let bc_middleware_state = if let Some(path) = db_path {
2432 BehavioralCloningMiddlewareState::with_database_path(path)
2433 } else {
2434 BehavioralCloningMiddlewareState::new()
2435 };
2436
2437 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2439 .ok()
2440 .and_then(|v| v.parse::<bool>().ok())
2441 .unwrap_or(false);
2442
2443 if enabled {
2444 let bc_state_clone = bc_middleware_state.clone();
2445 app = app.layer(axum::middleware::from_fn(
2446 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2447 let state = bc_state_clone.clone();
2448 async move {
2449 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2451 req.extensions_mut().insert(state);
2452 }
2453 crate::middleware::behavioral_cloning::behavioral_cloning_middleware(
2455 req, next,
2456 )
2457 .await
2458 }
2459 },
2460 ));
2461 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2462 }
2463 }
2464
2465 {
2467 use crate::handlers::consumer_contracts::{
2468 consumer_contracts_router, ConsumerContractsState,
2469 };
2470 use mockforge_core::consumer_contracts::{
2471 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2472 };
2473 use std::sync::Arc;
2474
2475 let registry = Arc::new(ConsumerRegistry::default());
2477
2478 let usage_recorder = Arc::new(UsageRecorder::default());
2480
2481 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2483
2484 let consumer_state = ConsumerContractsState {
2485 registry,
2486 usage_recorder,
2487 detector,
2488 violations: Arc::new(RwLock::new(HashMap::new())),
2489 };
2490
2491 app = app.merge(consumer_contracts_router(consumer_state));
2492 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2493 }
2494
2495 #[cfg(feature = "behavioral-cloning")]
2497 {
2498 use crate::handlers::behavioral_cloning::{
2499 behavioral_cloning_router, BehavioralCloningState,
2500 };
2501 use std::path::PathBuf;
2502
2503 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2505 .ok()
2506 .map(PathBuf::from)
2507 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2508
2509 let bc_state = if let Some(path) = db_path {
2510 BehavioralCloningState::with_database_path(path)
2511 } else {
2512 BehavioralCloningState::new()
2513 };
2514
2515 app = app.merge(behavioral_cloning_router(bc_state));
2516 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2517 }
2518
2519 {
2521 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2522 use crate::handlers::consistency::{consistency_router, ConsistencyState};
2523 use mockforge_core::consistency::ConsistencyEngine;
2524 use std::sync::Arc;
2525
2526 let consistency_engine = Arc::new(ConsistencyEngine::new());
2528
2529 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2531 consistency_engine.register_adapter(http_adapter.clone()).await;
2532
2533 let consistency_state = ConsistencyState {
2535 engine: consistency_engine.clone(),
2536 };
2537
2538 use crate::handlers::xray::XRayState;
2540 let xray_state = Arc::new(XRayState {
2541 engine: consistency_engine.clone(),
2542 request_contexts: std::sync::Arc::new(RwLock::new(HashMap::new())),
2543 });
2544
2545 let consistency_middleware_state = ConsistencyMiddlewareState {
2547 engine: consistency_engine.clone(),
2548 adapter: http_adapter,
2549 xray_state: Some(xray_state.clone()),
2550 };
2551
2552 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2554 app = app.layer(axum::middleware::from_fn(
2555 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2556 let state = consistency_middleware_state_clone.clone();
2557 async move {
2558 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2560 req.extensions_mut().insert(state);
2561 }
2562 consistency::middleware::consistency_middleware(req, next).await
2564 }
2565 },
2566 ));
2567
2568 app = app.merge(consistency_router(consistency_state));
2570 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2571
2572 {
2574 use crate::handlers::fidelity::{fidelity_router, FidelityState};
2575 let fidelity_state = FidelityState::new();
2576 app = app.merge(fidelity_router(fidelity_state));
2577 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
2578 }
2579
2580 {
2582 use crate::handlers::scenario_studio::{scenario_studio_router, ScenarioStudioState};
2583 let scenario_studio_state = ScenarioStudioState::new();
2584 app = app.merge(scenario_studio_router(scenario_studio_state));
2585 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
2586 }
2587
2588 {
2590 use crate::handlers::performance::{performance_router, PerformanceState};
2591 let performance_state = PerformanceState::new();
2592 app = app.nest("/api/performance", performance_router(performance_state));
2593 debug!("Performance mode endpoints mounted at /api/performance");
2594 }
2595
2596 {
2598 use crate::handlers::world_state::{world_state_router, WorldStateState};
2599 use mockforge_world_state::WorldStateEngine;
2600 use std::sync::Arc;
2601 use tokio::sync::RwLock;
2602
2603 let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
2604 let world_state_state = WorldStateState {
2605 engine: world_state_engine,
2606 };
2607 app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
2608 debug!("World state endpoints mounted at /api/world-state");
2609 }
2610
2611 {
2613 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
2614 use mockforge_core::snapshots::SnapshotManager;
2615 use std::path::PathBuf;
2616
2617 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
2618 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
2619
2620 let snapshot_state = SnapshotState {
2621 manager: snapshot_manager,
2622 consistency_engine: Some(consistency_engine.clone()),
2623 workspace_persistence: None, vbr_engine: None, recorder: None, };
2627
2628 app = app.merge(snapshot_router(snapshot_state));
2629 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
2630
2631 {
2633 use crate::handlers::xray::xray_router;
2634 app = app.merge(xray_router((*xray_state).clone()));
2635 debug!("X-Ray API endpoints mounted at /api/v1/xray");
2636 }
2637 }
2638
2639 {
2641 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
2642 use crate::middleware::ab_testing::ab_testing_middleware;
2643
2644 let ab_testing_state = ABTestingState::new();
2645
2646 let ab_testing_state_clone = ab_testing_state.clone();
2648 app = app.layer(axum::middleware::from_fn(
2649 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2650 let state = ab_testing_state_clone.clone();
2651 async move {
2652 if req.extensions().get::<ABTestingState>().is_none() {
2654 req.extensions_mut().insert(state);
2655 }
2656 ab_testing_middleware(req, next).await
2658 }
2659 },
2660 ));
2661
2662 app = app.merge(ab_testing_router(ab_testing_state));
2664 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
2665 }
2666 }
2667
2668 {
2670 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
2671 use mockforge_core::pr_generation::{PRGenerator, PRProvider};
2672 use std::sync::Arc;
2673
2674 let pr_config = mockforge_core::pr_generation::PRGenerationConfig::from_env();
2676
2677 let generator = if pr_config.enabled && pr_config.token.is_some() {
2678 let token = pr_config.token.as_ref().unwrap().clone();
2679 let generator = match pr_config.provider {
2680 PRProvider::GitHub => PRGenerator::new_github(
2681 pr_config.owner.clone(),
2682 pr_config.repo.clone(),
2683 token,
2684 pr_config.base_branch.clone(),
2685 ),
2686 PRProvider::GitLab => PRGenerator::new_gitlab(
2687 pr_config.owner.clone(),
2688 pr_config.repo.clone(),
2689 token,
2690 pr_config.base_branch.clone(),
2691 ),
2692 };
2693 Some(Arc::new(generator))
2694 } else {
2695 None
2696 };
2697
2698 let pr_state = PRGenerationState {
2699 generator: generator.clone(),
2700 };
2701
2702 app = app.merge(pr_generation_router(pr_state));
2703 if generator.is_some() {
2704 debug!(
2705 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
2706 pr_config.provider
2707 );
2708 } else {
2709 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
2710 }
2711 }
2712
2713 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
2715
2716 if let Some(mt_config) = multi_tenant_config {
2718 if mt_config.enabled {
2719 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
2720 use std::sync::Arc;
2721
2722 info!(
2723 "Multi-tenant mode enabled with {} routing strategy",
2724 match mt_config.routing_strategy {
2725 mockforge_core::RoutingStrategy::Path => "path-based",
2726 mockforge_core::RoutingStrategy::Port => "port-based",
2727 mockforge_core::RoutingStrategy::Both => "hybrid",
2728 }
2729 );
2730
2731 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
2733
2734 let default_workspace =
2736 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
2737 if let Err(e) =
2738 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
2739 {
2740 warn!("Failed to register default workspace: {}", e);
2741 } else {
2742 info!("Registered default workspace: '{}'", mt_config.default_workspace);
2743 }
2744
2745 let registry = Arc::new(registry);
2747
2748 let _workspace_router = WorkspaceRouter::new(registry);
2750 info!("Workspace routing middleware initialized for HTTP server");
2751 }
2752 }
2753
2754 let mut final_cors_config = cors_config;
2756 let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
2757 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
2759 let mut rate_limit_config = middleware::RateLimitConfig {
2760 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
2761 .ok()
2762 .and_then(|v| v.parse().ok())
2763 .unwrap_or(1000),
2764 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
2765 .ok()
2766 .and_then(|v| v.parse().ok())
2767 .unwrap_or(2000),
2768 per_ip: true,
2769 per_endpoint: false,
2770 };
2771
2772 if let Some(deploy_config) = &deceptive_deploy_config {
2773 if deploy_config.enabled {
2774 info!("Deceptive deploy mode enabled - applying production-like configuration");
2775
2776 if let Some(prod_cors) = &deploy_config.cors {
2778 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
2779 enabled: true,
2780 allowed_origins: prod_cors.allowed_origins.clone(),
2781 allowed_methods: prod_cors.allowed_methods.clone(),
2782 allowed_headers: prod_cors.allowed_headers.clone(),
2783 allow_credentials: prod_cors.allow_credentials,
2784 });
2785 info!("Applied production-like CORS configuration");
2786 }
2787
2788 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
2790 rate_limit_config = middleware::RateLimitConfig {
2791 requests_per_minute: prod_rate_limit.requests_per_minute,
2792 burst: prod_rate_limit.burst,
2793 per_ip: prod_rate_limit.per_ip,
2794 per_endpoint: false,
2795 };
2796 info!(
2797 "Applied production-like rate limiting: {} req/min, burst: {}",
2798 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
2799 );
2800 }
2801
2802 if !deploy_config.headers.is_empty() {
2804 let headers_map: HashMap<String, String> = deploy_config.headers.clone();
2805 production_headers = Some(std::sync::Arc::new(headers_map));
2806 info!("Configured {} production headers", deploy_config.headers.len());
2807 }
2808
2809 if let Some(prod_oauth) = &deploy_config.oauth {
2811 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
2812 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
2813 oauth2: Some(oauth2_config),
2814 ..Default::default()
2815 });
2816 info!("Applied production-like OAuth configuration for deceptive deploy");
2817 }
2818 }
2819 }
2820
2821 let rate_limiter =
2823 std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
2824
2825 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
2826
2827 if let Some(headers) = production_headers.clone() {
2829 state = state.with_production_headers(headers);
2830 }
2831
2832 app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
2834
2835 if state.production_headers.is_some() {
2837 app =
2838 app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
2839 }
2840
2841 if let Some(auth_config) = deceptive_deploy_auth_config {
2843 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
2844 use std::collections::HashMap;
2845 use std::sync::Arc;
2846 use tokio::sync::RwLock;
2847
2848 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
2850 match create_oauth2_client(oauth2_config) {
2851 Ok(client) => Some(client),
2852 Err(e) => {
2853 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
2854 None
2855 }
2856 }
2857 } else {
2858 None
2859 };
2860
2861 let auth_state = AuthState {
2863 config: auth_config,
2864 spec: None, oauth2_client,
2866 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
2867 };
2868
2869 app = app.layer(from_fn_with_state(auth_state, auth_middleware));
2871 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
2872 }
2873
2874 #[cfg(feature = "runtime-daemon")]
2876 {
2877 use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
2878 use std::sync::Arc;
2879
2880 let daemon_config = RuntimeDaemonConfig::from_env();
2882
2883 if daemon_config.enabled {
2884 info!("Runtime daemon enabled - auto-creating mocks from 404s");
2885
2886 let management_api_url =
2888 std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
2889 let port =
2890 std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
2891 format!("http://localhost:{}", port)
2892 });
2893
2894 let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
2896
2897 let detector = NotFoundDetector::new(daemon_config.clone());
2899 detector.set_generator(generator).await;
2900
2901 let detector_clone = detector.clone();
2903 app = app.layer(axum::middleware::from_fn(
2904 move |req: axum::extract::Request, next: axum::middleware::Next| {
2905 let detector = detector_clone.clone();
2906 async move { detector.detect_and_auto_create(req, next).await }
2907 },
2908 ));
2909
2910 debug!("Runtime daemon 404 detection middleware added");
2911 }
2912 }
2913
2914 {
2916 let routes_state = HttpServerState::with_routes(captured_routes);
2917 let routes_router = Router::new()
2918 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
2919 .with_state(routes_state);
2920 app = app.merge(routes_router);
2921 }
2922
2923 app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
2925
2926 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
2931
2932 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
2935
2936 app = apply_cors_middleware(app, final_cors_config);
2938
2939 app
2940}
2941
2942#[test]
2946fn test_route_info_clone() {
2947 let route = RouteInfo {
2948 method: "POST".to_string(),
2949 path: "/users".to_string(),
2950 operation_id: Some("createUser".to_string()),
2951 summary: None,
2952 description: None,
2953 parameters: vec![],
2954 };
2955
2956 let cloned = route.clone();
2957 assert_eq!(route.method, cloned.method);
2958 assert_eq!(route.path, cloned.path);
2959 assert_eq!(route.operation_id, cloned.operation_id);
2960}
2961
2962#[test]
2963fn test_http_server_state_new() {
2964 let state = HttpServerState::new();
2965 assert_eq!(state.routes.len(), 0);
2966}
2967
2968#[test]
2969fn test_http_server_state_with_routes() {
2970 let routes = vec![
2971 RouteInfo {
2972 method: "GET".to_string(),
2973 path: "/users".to_string(),
2974 operation_id: Some("getUsers".to_string()),
2975 summary: None,
2976 description: None,
2977 parameters: vec![],
2978 },
2979 RouteInfo {
2980 method: "POST".to_string(),
2981 path: "/users".to_string(),
2982 operation_id: Some("createUser".to_string()),
2983 summary: None,
2984 description: None,
2985 parameters: vec![],
2986 },
2987 ];
2988
2989 let state = HttpServerState::with_routes(routes.clone());
2990 assert_eq!(state.routes.len(), 2);
2991 assert_eq!(state.routes[0].method, "GET");
2992 assert_eq!(state.routes[1].method, "POST");
2993}
2994
2995#[test]
2996fn test_http_server_state_clone() {
2997 let routes = vec![RouteInfo {
2998 method: "GET".to_string(),
2999 path: "/test".to_string(),
3000 operation_id: None,
3001 summary: None,
3002 description: None,
3003 parameters: vec![],
3004 }];
3005
3006 let state = HttpServerState::with_routes(routes);
3007 let cloned = state.clone();
3008
3009 assert_eq!(state.routes.len(), cloned.routes.len());
3010 assert_eq!(state.routes[0].method, cloned.routes[0].method);
3011}
3012
3013#[tokio::test]
3014async fn test_build_router_without_openapi() {
3015 let _router = build_router(None, None, None).await;
3016 }
3018
3019#[tokio::test]
3020async fn test_build_router_with_nonexistent_spec() {
3021 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3022 }
3024
3025#[tokio::test]
3026async fn test_build_router_with_auth_and_latency() {
3027 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3028 }
3030
3031#[tokio::test]
3032async fn test_build_router_with_latency() {
3033 let _router = build_router_with_latency(None, None, None).await;
3034 }
3036
3037#[tokio::test]
3038async fn test_build_router_with_auth() {
3039 let _router = build_router_with_auth(None, None, None).await;
3040 }
3042
3043#[tokio::test]
3044async fn test_build_router_with_chains() {
3045 let _router = build_router_with_chains(None, None, None).await;
3046 }
3048
3049#[test]
3050fn test_route_info_with_all_fields() {
3051 let route = RouteInfo {
3052 method: "PUT".to_string(),
3053 path: "/users/{id}".to_string(),
3054 operation_id: Some("updateUser".to_string()),
3055 summary: Some("Update user".to_string()),
3056 description: Some("Updates an existing user".to_string()),
3057 parameters: vec!["id".to_string(), "body".to_string()],
3058 };
3059
3060 assert!(route.operation_id.is_some());
3061 assert!(route.summary.is_some());
3062 assert!(route.description.is_some());
3063 assert_eq!(route.parameters.len(), 2);
3064}
3065
3066#[test]
3067fn test_route_info_with_minimal_fields() {
3068 let route = RouteInfo {
3069 method: "DELETE".to_string(),
3070 path: "/users/{id}".to_string(),
3071 operation_id: None,
3072 summary: None,
3073 description: None,
3074 parameters: vec![],
3075 };
3076
3077 assert!(route.operation_id.is_none());
3078 assert!(route.summary.is_none());
3079 assert!(route.description.is_none());
3080 assert_eq!(route.parameters.len(), 0);
3081}
3082
3083#[test]
3084fn test_http_server_state_empty_routes() {
3085 let state = HttpServerState::with_routes(vec![]);
3086 assert_eq!(state.routes.len(), 0);
3087}
3088
3089#[test]
3090fn test_http_server_state_multiple_routes() {
3091 let routes = vec![
3092 RouteInfo {
3093 method: "GET".to_string(),
3094 path: "/users".to_string(),
3095 operation_id: Some("listUsers".to_string()),
3096 summary: Some("List all users".to_string()),
3097 description: None,
3098 parameters: vec![],
3099 },
3100 RouteInfo {
3101 method: "GET".to_string(),
3102 path: "/users/{id}".to_string(),
3103 operation_id: Some("getUser".to_string()),
3104 summary: Some("Get a user".to_string()),
3105 description: None,
3106 parameters: vec!["id".to_string()],
3107 },
3108 RouteInfo {
3109 method: "POST".to_string(),
3110 path: "/users".to_string(),
3111 operation_id: Some("createUser".to_string()),
3112 summary: Some("Create a user".to_string()),
3113 description: None,
3114 parameters: vec!["body".to_string()],
3115 },
3116 ];
3117
3118 let state = HttpServerState::with_routes(routes);
3119 assert_eq!(state.routes.len(), 3);
3120
3121 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3123 assert!(methods.contains(&"GET"));
3124 assert!(methods.contains(&"POST"));
3125}
3126
3127#[test]
3128fn test_http_server_state_with_rate_limiter() {
3129 use std::sync::Arc;
3130
3131 let config = crate::middleware::RateLimitConfig::default();
3132 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
3133
3134 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3135
3136 assert!(state.rate_limiter.is_some());
3137 assert_eq!(state.routes.len(), 0);
3138}
3139
3140#[tokio::test]
3141async fn test_build_router_includes_rate_limiter() {
3142 let _router = build_router(None, None, None).await;
3143 }