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