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::http::StatusCode;
289use axum::middleware::from_fn_with_state;
290use axum::response::Json;
291use axum::Router;
292use mockforge_core::failure_injection::{FailureConfig, FailureInjector};
293use mockforge_core::intelligent_behavior::config::Persona;
294use mockforge_core::latency::LatencyInjector;
295use mockforge_core::openapi::OpenApiSpec;
296use mockforge_core::openapi_routes::OpenApiRouteRegistry;
297use mockforge_core::openapi_routes::ValidationOptions;
298use std::sync::Arc;
299use tower_http::cors::{Any, CorsLayer};
300
301use mockforge_core::LatencyProfile;
302#[cfg(feature = "data-faker")]
303use mockforge_data::provider::register_core_faker_provider;
304use std::collections::HashMap;
305use std::ffi::OsStr;
306use std::path::Path;
307use tokio::fs;
308use tokio::sync::RwLock;
309use tracing::*;
310
311#[derive(Clone)]
313pub struct RouteInfo {
314 pub method: String,
316 pub path: String,
318 pub operation_id: Option<String>,
320 pub summary: Option<String>,
322 pub description: Option<String>,
324 pub parameters: Vec<String>,
326}
327
328#[derive(Clone)]
330pub struct HttpServerState {
331 pub routes: Vec<RouteInfo>,
333 pub rate_limiter: Option<std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>>,
335 pub production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>>,
337}
338
339impl Default for HttpServerState {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345impl HttpServerState {
346 pub fn new() -> Self {
348 Self {
349 routes: Vec::new(),
350 rate_limiter: None,
351 production_headers: None,
352 }
353 }
354
355 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
357 Self {
358 routes,
359 rate_limiter: None,
360 production_headers: None,
361 }
362 }
363
364 pub fn with_rate_limiter(
366 mut self,
367 rate_limiter: std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>,
368 ) -> Self {
369 self.rate_limiter = Some(rate_limiter);
370 self
371 }
372
373 pub fn with_production_headers(
375 mut self,
376 headers: std::sync::Arc<std::collections::HashMap<String, String>>,
377 ) -> Self {
378 self.production_headers = Some(headers);
379 self
380 }
381}
382
383async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
385 let route_info: Vec<serde_json::Value> = state
386 .routes
387 .iter()
388 .map(|route| {
389 serde_json::json!({
390 "method": route.method,
391 "path": route.path,
392 "operation_id": route.operation_id,
393 "summary": route.summary,
394 "description": route.description,
395 "parameters": route.parameters
396 })
397 })
398 .collect();
399
400 Json(serde_json::json!({
401 "routes": route_info,
402 "total": state.routes.len()
403 }))
404}
405
406pub async fn build_router(
408 spec_path: Option<String>,
409 options: Option<ValidationOptions>,
410 failure_config: Option<FailureConfig>,
411) -> Router {
412 build_router_with_multi_tenant(
413 spec_path,
414 options,
415 failure_config,
416 None,
417 None,
418 None,
419 None,
420 None,
421 None,
422 None,
423 )
424 .await
425}
426
427fn apply_cors_middleware(
429 app: Router,
430 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
431) -> Router {
432 use http::Method;
433 use tower_http::cors::AllowOrigin;
434
435 if let Some(config) = cors_config {
436 if !config.enabled {
437 return app;
438 }
439
440 let mut cors_layer = CorsLayer::new();
441 let mut is_wildcard_origin = false;
442
443 if config.allowed_origins.contains(&"*".to_string()) {
445 cors_layer = cors_layer.allow_origin(Any);
446 is_wildcard_origin = true;
447 } else if !config.allowed_origins.is_empty() {
448 let origins: Vec<_> = config
450 .allowed_origins
451 .iter()
452 .filter_map(|origin| {
453 origin.parse::<http::HeaderValue>().ok().map(|hv| AllowOrigin::exact(hv))
454 })
455 .collect();
456
457 if origins.is_empty() {
458 warn!("No valid CORS origins configured, using permissive CORS");
460 cors_layer = cors_layer.allow_origin(Any);
461 is_wildcard_origin = true;
462 } else {
463 if origins.len() == 1 {
466 cors_layer = cors_layer.allow_origin(origins[0].clone());
467 is_wildcard_origin = false;
468 } else {
469 warn!(
471 "Multiple CORS origins configured, using permissive CORS. \
472 Consider using '*' for all origins."
473 );
474 cors_layer = cors_layer.allow_origin(Any);
475 is_wildcard_origin = true;
476 }
477 }
478 } else {
479 cors_layer = cors_layer.allow_origin(Any);
481 is_wildcard_origin = true;
482 }
483
484 if !config.allowed_methods.is_empty() {
486 let methods: Vec<Method> =
487 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
488 if !methods.is_empty() {
489 cors_layer = cors_layer.allow_methods(methods);
490 }
491 } else {
492 cors_layer = cors_layer.allow_methods([
494 Method::GET,
495 Method::POST,
496 Method::PUT,
497 Method::DELETE,
498 Method::PATCH,
499 Method::OPTIONS,
500 ]);
501 }
502
503 if !config.allowed_headers.is_empty() {
505 let headers: Vec<_> = config
506 .allowed_headers
507 .iter()
508 .filter_map(|h| h.parse::<http::HeaderName>().ok())
509 .collect();
510 if !headers.is_empty() {
511 cors_layer = cors_layer.allow_headers(headers);
512 }
513 } else {
514 cors_layer =
516 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
517 }
518
519 let should_allow_credentials = if is_wildcard_origin {
523 false
525 } else {
526 config.allow_credentials
528 };
529
530 cors_layer = cors_layer.allow_credentials(should_allow_credentials);
531
532 info!(
533 "CORS middleware enabled with configured settings (credentials: {})",
534 should_allow_credentials
535 );
536 app.layer(cors_layer)
537 } else {
538 debug!("No CORS config provided, using permissive CORS for development");
542 app.layer(CorsLayer::permissive().allow_credentials(false))
545 }
546}
547
548#[allow(clippy::too_many_arguments)]
550pub async fn build_router_with_multi_tenant(
551 spec_path: Option<String>,
552 options: Option<ValidationOptions>,
553 failure_config: Option<FailureConfig>,
554 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
555 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
556 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
557 ai_generator: Option<
558 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
559 >,
560 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
561 mockai: Option<
562 std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
563 >,
564 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
565) -> Router {
566 use std::time::Instant;
567
568 let startup_start = Instant::now();
569
570 let mut app = Router::new();
572
573 let mut rate_limit_config = crate::middleware::RateLimitConfig {
576 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
577 .ok()
578 .and_then(|v| v.parse().ok())
579 .unwrap_or(1000),
580 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
581 .ok()
582 .and_then(|v| v.parse().ok())
583 .unwrap_or(2000),
584 per_ip: true,
585 per_endpoint: false,
586 };
587
588 let mut final_cors_config = cors_config;
590 let mut production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>> =
591 None;
592 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
594
595 if let Some(deploy_config) = &deceptive_deploy_config {
596 if deploy_config.enabled {
597 info!("Deceptive deploy mode enabled - applying production-like configuration");
598
599 if let Some(prod_cors) = &deploy_config.cors {
601 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
602 enabled: true,
603 allowed_origins: prod_cors.allowed_origins.clone(),
604 allowed_methods: prod_cors.allowed_methods.clone(),
605 allowed_headers: prod_cors.allowed_headers.clone(),
606 allow_credentials: prod_cors.allow_credentials,
607 });
608 info!("Applied production-like CORS configuration");
609 }
610
611 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
613 rate_limit_config = crate::middleware::RateLimitConfig {
614 requests_per_minute: prod_rate_limit.requests_per_minute,
615 burst: prod_rate_limit.burst,
616 per_ip: prod_rate_limit.per_ip,
617 per_endpoint: false,
618 };
619 info!(
620 "Applied production-like rate limiting: {} req/min, burst: {}",
621 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
622 );
623 }
624
625 if !deploy_config.headers.is_empty() {
627 let headers_map: std::collections::HashMap<String, String> =
628 deploy_config.headers.clone();
629 production_headers = Some(std::sync::Arc::new(headers_map));
630 info!("Configured {} production headers", deploy_config.headers.len());
631 }
632
633 if let Some(prod_oauth) = &deploy_config.oauth {
635 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
636 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
637 oauth2: Some(oauth2_config),
638 ..Default::default()
639 });
640 info!("Applied production-like OAuth configuration for deceptive deploy");
641 }
642 }
643 }
644
645 let rate_limiter =
646 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
647
648 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
649
650 if let Some(headers) = production_headers.clone() {
652 state = state.with_production_headers(headers);
653 }
654
655 let spec_path_for_mgmt = spec_path.clone();
657
658 if let Some(spec_path) = spec_path {
660 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
661
662 let spec_load_start = Instant::now();
664 match OpenApiSpec::from_file(&spec_path).await {
665 Ok(openapi) => {
666 let spec_load_duration = spec_load_start.elapsed();
667 info!(
668 "Successfully loaded OpenAPI spec from {} (took {:?})",
669 spec_path, spec_load_duration
670 );
671
672 tracing::debug!("Creating OpenAPI route registry...");
674 let registry_start = Instant::now();
675
676 let persona = load_persona_from_config().await;
678
679 let registry = if let Some(opts) = options {
680 tracing::debug!("Using custom validation options");
681 if let Some(ref persona) = persona {
682 tracing::info!("Using persona '{}' for route generation", persona.name);
683 }
684 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
685 } else {
686 tracing::debug!("Using environment-based options");
687 if let Some(ref persona) = persona {
688 tracing::info!("Using persona '{}' for route generation", persona.name);
689 }
690 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
691 };
692 let registry_duration = registry_start.elapsed();
693 info!(
694 "Created OpenAPI route registry with {} routes (took {:?})",
695 registry.routes().len(),
696 registry_duration
697 );
698
699 let extract_start = Instant::now();
701 let route_info: Vec<RouteInfo> = registry
702 .routes()
703 .iter()
704 .map(|route| RouteInfo {
705 method: route.method.clone(),
706 path: route.path.clone(),
707 operation_id: route.operation.operation_id.clone(),
708 summary: route.operation.summary.clone(),
709 description: route.operation.description.clone(),
710 parameters: route.parameters.clone(),
711 })
712 .collect();
713 state.routes = route_info;
714 let extract_duration = extract_start.elapsed();
715 debug!("Extracted route information (took {:?})", extract_duration);
716
717 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
719 tracing::debug!("Loading overrides from environment variable");
720 let overrides_start = Instant::now();
721 match mockforge_core::Overrides::load_from_globs(&[]).await {
722 Ok(overrides) => {
723 let overrides_duration = overrides_start.elapsed();
724 info!(
725 "Loaded {} override rules (took {:?})",
726 overrides.rules().len(),
727 overrides_duration
728 );
729 Some(overrides)
730 }
731 Err(e) => {
732 tracing::warn!("Failed to load overrides: {}", e);
733 None
734 }
735 }
736 } else {
737 None
738 };
739
740 let router_build_start = Instant::now();
742 let overrides_enabled = overrides.is_some();
743 let openapi_router = if let Some(mockai_instance) = &mockai {
744 tracing::debug!("Building router with MockAI support");
745 registry.build_router_with_mockai(Some(mockai_instance.clone()))
746 } else if let Some(ai_generator) = &ai_generator {
747 tracing::debug!("Building router with AI generator support");
748 registry.build_router_with_ai(Some(ai_generator.clone()))
749 } else if let Some(failure_config) = &failure_config {
750 tracing::debug!("Building router with failure injection and overrides");
751 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
752 registry.build_router_with_injectors_and_overrides(
753 LatencyInjector::default(),
754 Some(failure_injector),
755 overrides,
756 overrides_enabled,
757 )
758 } else {
759 tracing::debug!("Building router with overrides");
760 registry.build_router_with_injectors_and_overrides(
761 LatencyInjector::default(),
762 None,
763 overrides,
764 overrides_enabled,
765 )
766 };
767 let router_build_duration = router_build_start.elapsed();
768 debug!("Built OpenAPI router (took {:?})", router_build_duration);
769
770 tracing::debug!("Merging OpenAPI router with main router");
771 app = app.merge(openapi_router);
772 tracing::debug!("Router built successfully");
773 }
774 Err(e) => {
775 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
776 }
777 }
778 }
779
780 app = app.route(
782 "/health",
783 axum::routing::get(|| async {
784 use mockforge_core::server_utils::health::HealthStatus;
785 {
786 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
788 Ok(value) => axum::Json(value),
789 Err(e) => {
790 tracing::error!("Failed to serialize health status: {}", e);
792 axum::Json(serde_json::json!({
793 "status": "healthy",
794 "service": "mockforge-http",
795 "uptime_seconds": 0
796 }))
797 }
798 }
799 }
800 }),
801 )
802 .merge(sse::sse_router())
804 .merge(file_server::file_serving_router());
806
807 let state_for_routes = state.clone();
809
810 let routes_router = Router::new()
812 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
813 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
814 .with_state(state_for_routes);
815
816 app = app.merge(routes_router);
818
819 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
822 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
823
824 if std::path::Path::new(&coverage_html_path).exists() {
826 app = app.nest_service(
827 "/__mockforge/coverage.html",
828 tower_http::services::ServeFile::new(&coverage_html_path),
829 );
830 debug!("Serving coverage UI from: {}", coverage_html_path);
831 } else {
832 debug!(
833 "Coverage UI file not found at: {}. Skipping static file serving.",
834 coverage_html_path
835 );
836 }
837
838 let mut management_state = ManagementState::new(None, spec_path_for_mgmt, 3000); use std::sync::Arc;
843 let ws_state = WsManagementState::new();
844 let ws_broadcast = Arc::new(ws_state.tx.clone());
845 let management_state = management_state.with_ws_broadcast(ws_broadcast);
846
847 #[cfg(feature = "smtp")]
851 let management_state = {
852 if let Some(smtp_reg) = smtp_registry {
853 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
854 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
855 Err(e) => {
856 error!(
857 "Invalid SMTP registry type passed to HTTP management state: {:?}",
858 e.type_id()
859 );
860 management_state
861 }
862 }
863 } else {
864 management_state
865 }
866 };
867 #[cfg(not(feature = "smtp"))]
868 let management_state = management_state;
869 #[cfg(not(feature = "smtp"))]
870 let _ = smtp_registry;
871 app = app.nest("/__mockforge/api", management_router(management_state));
872
873 app = app.merge(verification_router());
875
876 use crate::auth::oidc::oidc_router;
878 app = app.merge(oidc_router());
879
880 {
882 use mockforge_core::security::get_global_access_review_service;
883 if let Some(service) = get_global_access_review_service().await {
884 use crate::handlers::access_review::{access_review_router, AccessReviewState};
885 let review_state = AccessReviewState { service };
886 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
887 debug!("Access review API mounted at /api/v1/security/access-reviews");
888 }
889 }
890
891 {
893 use mockforge_core::security::get_global_privileged_access_manager;
894 if let Some(manager) = get_global_privileged_access_manager().await {
895 use crate::handlers::privileged_access::{
896 privileged_access_router, PrivilegedAccessState,
897 };
898 let privileged_state = PrivilegedAccessState { manager };
899 app = app.nest(
900 "/api/v1/security/privileged-access",
901 privileged_access_router(privileged_state),
902 );
903 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
904 }
905 }
906
907 {
909 use mockforge_core::security::get_global_change_management_engine;
910 if let Some(engine) = get_global_change_management_engine().await {
911 use crate::handlers::change_management::{
912 change_management_router, ChangeManagementState,
913 };
914 let change_state = ChangeManagementState { engine };
915 app = app.nest("/api/v1/change-management", change_management_router(change_state));
916 debug!("Change management API mounted at /api/v1/change-management");
917 }
918 }
919
920 {
922 use mockforge_core::security::get_global_risk_assessment_engine;
923 if let Some(engine) = get_global_risk_assessment_engine().await {
924 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
925 let risk_state = RiskAssessmentState { engine };
926 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
927 debug!("Risk assessment API mounted at /api/v1/security/risks");
928 }
929 }
930
931 {
933 use crate::auth::token_lifecycle::TokenLifecycleManager;
934 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
935 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
936 let lifecycle_state = TokenLifecycleState {
937 manager: lifecycle_manager,
938 };
939 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
940 debug!("Token lifecycle API mounted at /api/v1/auth");
941 }
942
943 {
945 use crate::auth::oidc::{load_oidc_state, OidcState};
946 use crate::auth::token_lifecycle::TokenLifecycleManager;
947 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
948 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
950 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
951 let oauth2_state = OAuth2ServerState {
952 oidc_state,
953 lifecycle_manager,
954 auth_codes: Arc::new(RwLock::new(HashMap::new())),
955 };
956 app = app.merge(oauth2_server_router(oauth2_state));
957 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
958 }
959
960 {
962 use crate::auth::oidc::{load_oidc_state, OidcState};
963 use crate::auth::risk_engine::RiskEngine;
964 use crate::auth::token_lifecycle::TokenLifecycleManager;
965 use crate::handlers::consent::{consent_router, ConsentState};
966 use crate::handlers::oauth2_server::OAuth2ServerState;
967 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
969 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
970 let oauth2_state = OAuth2ServerState {
971 oidc_state: oidc_state.clone(),
972 lifecycle_manager: lifecycle_manager.clone(),
973 auth_codes: Arc::new(RwLock::new(HashMap::new())),
974 };
975 let risk_engine = Arc::new(RiskEngine::default());
976 let consent_state = ConsentState {
977 oauth2_state,
978 risk_engine,
979 };
980 app = app.merge(consent_router(consent_state));
981 debug!("Consent screen endpoints mounted at /consent");
982 }
983
984 {
986 use crate::auth::risk_engine::RiskEngine;
987 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
988 let risk_engine = Arc::new(RiskEngine::default());
989 let risk_state = RiskSimulationState { risk_engine };
990 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
991 debug!("Risk simulation API mounted at /api/v1/auth/risk");
992 }
993
994 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
996
997 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
999
1000 app = app.layer(axum::middleware::from_fn(crate::middleware::security_middleware));
1002
1003 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1006
1007 app = app.layer(from_fn_with_state(state.clone(), crate::middleware::rate_limit_middleware));
1009
1010 if state.production_headers.is_some() {
1012 app = app.layer(from_fn_with_state(
1013 state.clone(),
1014 crate::middleware::production_headers_middleware,
1015 ));
1016 }
1017
1018 if let Some(auth_config) = deceptive_deploy_auth_config {
1020 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1021 use std::collections::HashMap;
1022 use std::sync::Arc;
1023 use tokio::sync::RwLock;
1024
1025 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1027 match create_oauth2_client(oauth2_config) {
1028 Ok(client) => Some(client),
1029 Err(e) => {
1030 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1031 None
1032 }
1033 }
1034 } else {
1035 None
1036 };
1037
1038 let auth_state = AuthState {
1040 config: auth_config,
1041 spec: None, oauth2_client,
1043 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1044 };
1045
1046 app = app.layer(axum::middleware::from_fn_with_state(auth_state, auth_middleware));
1048 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1049 }
1050
1051 app = apply_cors_middleware(app, final_cors_config);
1053
1054 if let Some(mt_config) = multi_tenant_config {
1056 if mt_config.enabled {
1057 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1058 use std::sync::Arc;
1059
1060 info!(
1061 "Multi-tenant mode enabled with {} routing strategy",
1062 match mt_config.routing_strategy {
1063 mockforge_core::RoutingStrategy::Path => "path-based",
1064 mockforge_core::RoutingStrategy::Port => "port-based",
1065 mockforge_core::RoutingStrategy::Both => "hybrid",
1066 }
1067 );
1068
1069 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1071
1072 let default_workspace =
1074 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1075 if let Err(e) =
1076 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1077 {
1078 warn!("Failed to register default workspace: {}", e);
1079 } else {
1080 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1081 }
1082
1083 if mt_config.auto_discover {
1085 if let Some(config_dir) = &mt_config.config_directory {
1086 let config_path = Path::new(config_dir);
1087 if config_path.exists() && config_path.is_dir() {
1088 match fs::read_dir(config_path).await {
1089 Ok(mut entries) => {
1090 while let Ok(Some(entry)) = entries.next_entry().await {
1091 let path = entry.path();
1092 if path.extension() == Some(OsStr::new("yaml")) {
1093 match fs::read_to_string(&path).await {
1094 Ok(content) => {
1095 match serde_yaml::from_str::<
1096 mockforge_core::Workspace,
1097 >(
1098 &content
1099 ) {
1100 Ok(workspace) => {
1101 if let Err(e) = registry.register_workspace(
1102 workspace.id.clone(),
1103 workspace,
1104 ) {
1105 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1106 } else {
1107 info!("Auto-registered workspace from {:?}", path);
1108 }
1109 }
1110 Err(e) => {
1111 warn!("Failed to parse workspace from {:?}: {}", path, e);
1112 }
1113 }
1114 }
1115 Err(e) => {
1116 warn!(
1117 "Failed to read workspace file {:?}: {}",
1118 path, e
1119 );
1120 }
1121 }
1122 }
1123 }
1124 }
1125 Err(e) => {
1126 warn!("Failed to read config directory {:?}: {}", config_path, e);
1127 }
1128 }
1129 } else {
1130 warn!(
1131 "Config directory {:?} does not exist or is not a directory",
1132 config_path
1133 );
1134 }
1135 }
1136 }
1137
1138 let registry = Arc::new(registry);
1140
1141 let _workspace_router = WorkspaceRouter::new(registry);
1143
1144 info!("Workspace routing middleware initialized for HTTP server");
1147 }
1148 }
1149
1150 let total_startup_duration = startup_start.elapsed();
1151 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1152
1153 app
1154}
1155
1156pub async fn build_router_with_auth_and_latency(
1158 _spec_path: Option<String>,
1159 _options: Option<()>,
1160 _auth_config: Option<mockforge_core::config::AuthConfig>,
1161 _latency_injector: Option<LatencyInjector>,
1162) -> Router {
1163 build_router(None, None, None).await
1165}
1166
1167pub async fn build_router_with_latency(
1169 _spec_path: Option<String>,
1170 _options: Option<ValidationOptions>,
1171 _latency_injector: Option<LatencyInjector>,
1172) -> Router {
1173 build_router(None, None, None).await
1175}
1176
1177pub async fn build_router_with_auth(
1179 spec_path: Option<String>,
1180 options: Option<ValidationOptions>,
1181 auth_config: Option<mockforge_core::config::AuthConfig>,
1182) -> Router {
1183 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1184 use std::sync::Arc;
1185
1186 #[cfg(feature = "data-faker")]
1188 {
1189 register_core_faker_provider();
1190 }
1191
1192 let spec = if let Some(spec_path) = &spec_path {
1194 match mockforge_core::openapi::OpenApiSpec::from_file(&spec_path).await {
1195 Ok(spec) => Some(Arc::new(spec)),
1196 Err(e) => {
1197 warn!("Failed to load OpenAPI spec for auth: {}", e);
1198 None
1199 }
1200 }
1201 } else {
1202 None
1203 };
1204
1205 let oauth2_client = if let Some(auth_config) = &auth_config {
1207 if let Some(oauth2_config) = &auth_config.oauth2 {
1208 match create_oauth2_client(oauth2_config) {
1209 Ok(client) => Some(client),
1210 Err(e) => {
1211 warn!("Failed to create OAuth2 client: {}", e);
1212 None
1213 }
1214 }
1215 } else {
1216 None
1217 }
1218 } else {
1219 None
1220 };
1221
1222 let auth_state = AuthState {
1223 config: auth_config.unwrap_or_default(),
1224 spec,
1225 oauth2_client,
1226 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1227 };
1228
1229 let mut app = Router::new().with_state(auth_state.clone());
1231
1232 if let Some(spec_path) = spec_path {
1234 match OpenApiSpec::from_file(&spec_path).await {
1235 Ok(openapi) => {
1236 info!("Loaded OpenAPI spec from {}", spec_path);
1237 let registry = if let Some(opts) = options {
1238 OpenApiRouteRegistry::new_with_options(openapi, opts)
1239 } else {
1240 OpenApiRouteRegistry::new_with_env(openapi)
1241 };
1242
1243 app = registry.build_router();
1244 }
1245 Err(e) => {
1246 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1247 }
1248 }
1249 }
1250
1251 app = app.route(
1253 "/health",
1254 axum::routing::get(|| async {
1255 use mockforge_core::server_utils::health::HealthStatus;
1256 {
1257 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1259 Ok(value) => axum::Json(value),
1260 Err(e) => {
1261 tracing::error!("Failed to serialize health status: {}", e);
1263 axum::Json(serde_json::json!({
1264 "status": "healthy",
1265 "service": "mockforge-http",
1266 "uptime_seconds": 0
1267 }))
1268 }
1269 }
1270 }
1271 }),
1272 )
1273 .merge(sse::sse_router())
1275 .merge(file_server::file_serving_router())
1277 .layer(axum::middleware::from_fn_with_state(auth_state.clone(), auth_middleware))
1279 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1281
1282 app
1283}
1284
1285pub async fn serve_router(
1287 port: u16,
1288 app: Router,
1289) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1290 serve_router_with_tls(port, app, None).await
1291}
1292
1293pub async fn serve_router_with_tls(
1295 port: u16,
1296 app: Router,
1297 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1298) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1299 use std::net::SocketAddr;
1300
1301 let addr = mockforge_core::wildcard_socket_addr(port);
1302
1303 if let Some(ref tls) = tls_config {
1304 if tls.enabled {
1305 info!("HTTPS listening on {}", addr);
1306 return serve_with_tls(addr, app, tls).await;
1307 }
1308 }
1309
1310 info!("HTTP listening on {}", addr);
1311
1312 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1313 format!(
1314 "Failed to bind HTTP server to port {}: {}\n\
1315 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 {}",
1316 port, e, port, port
1317 )
1318 })?;
1319
1320 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
1321 Ok(())
1322}
1323
1324async fn serve_with_tls(
1330 addr: std::net::SocketAddr,
1331 _app: Router,
1332 tls_config: &mockforge_core::config::HttpTlsConfig,
1333) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1334 let _acceptor = tls::load_tls_acceptor(tls_config)?;
1336
1337 Err(format!(
1340 "TLS/HTTPS support is configured but requires a reverse proxy (nginx) for production use.\n\
1341 Certificate validation passed: {} and {}\n\
1342 For native TLS support, please use a reverse proxy or wait for axum-server integration.\n\
1343 You can configure nginx with TLS termination pointing to the HTTP server on port {}.",
1344 tls_config.cert_file,
1345 tls_config.key_file,
1346 addr.port()
1347 )
1348 .into())
1349}
1350
1351pub async fn start(
1353 port: u16,
1354 spec_path: Option<String>,
1355 options: Option<ValidationOptions>,
1356) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1357 start_with_latency(port, spec_path, options, None).await
1358}
1359
1360pub async fn start_with_auth_and_latency(
1362 port: u16,
1363 spec_path: Option<String>,
1364 options: Option<ValidationOptions>,
1365 auth_config: Option<mockforge_core::config::AuthConfig>,
1366 latency_profile: Option<LatencyProfile>,
1367) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1368 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1369 .await
1370}
1371
1372pub async fn start_with_auth_and_injectors(
1374 port: u16,
1375 spec_path: Option<String>,
1376 options: Option<ValidationOptions>,
1377 auth_config: Option<mockforge_core::config::AuthConfig>,
1378 _latency_profile: Option<LatencyProfile>,
1379 _failure_injector: Option<mockforge_core::FailureInjector>,
1380) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1381 let app = build_router_with_auth(spec_path, options, auth_config).await;
1383 serve_router(port, app).await
1384}
1385
1386pub async fn start_with_latency(
1388 port: u16,
1389 spec_path: Option<String>,
1390 options: Option<ValidationOptions>,
1391 latency_profile: Option<LatencyProfile>,
1392) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1393 let latency_injector =
1394 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1395
1396 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1397 serve_router(port, app).await
1398}
1399
1400pub async fn build_router_with_chains(
1402 spec_path: Option<String>,
1403 options: Option<ValidationOptions>,
1404 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1405) -> Router {
1406 build_router_with_chains_and_multi_tenant(
1407 spec_path,
1408 options,
1409 circling_config,
1410 None,
1411 None,
1412 None,
1413 None,
1414 None,
1415 None,
1416 None,
1417 false,
1418 None, None, None, None, )
1423 .await
1424}
1425
1426#[allow(clippy::too_many_arguments)]
1428pub async fn build_router_with_chains_and_multi_tenant(
1429 spec_path: Option<String>,
1430 options: Option<ValidationOptions>,
1431 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1432 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
1433 route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1434 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1435 _ai_generator: Option<
1436 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
1437 >,
1438 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
1439 mqtt_broker: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
1440 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1441 traffic_shaping_enabled: bool,
1442 health_manager: Option<std::sync::Arc<health::HealthManager>>,
1443 _mockai: Option<
1444 std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
1445 >,
1446 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1447 proxy_config: Option<mockforge_core::proxy::config::ProxyConfig>,
1448) -> Router {
1449 use crate::latency_profiles::LatencyProfiles;
1450 use crate::op_middleware::Shared;
1451 use mockforge_core::Overrides;
1452
1453 let template_expand =
1455 options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1456 std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1457 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1458 .unwrap_or(false)
1459 });
1460
1461 let _shared = Shared {
1462 profiles: LatencyProfiles::default(),
1463 overrides: Overrides::default(),
1464 failure_injector: None,
1465 traffic_shaper,
1466 overrides_enabled: false,
1467 traffic_shaping_enabled,
1468 };
1469
1470 let mut app = Router::new();
1472 let mut include_default_health = true;
1473
1474 if let Some(ref spec) = spec_path {
1476 match OpenApiSpec::from_file(&spec).await {
1477 Ok(openapi) => {
1478 info!("Loaded OpenAPI spec from {}", spec);
1479
1480 let persona = load_persona_from_config().await;
1482
1483 let mut registry = if let Some(opts) = options {
1484 tracing::debug!("Using custom validation options");
1485 if let Some(ref persona) = persona {
1486 tracing::info!("Using persona '{}' for route generation", persona.name);
1487 }
1488 OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1489 } else {
1490 tracing::debug!("Using environment-based options");
1491 if let Some(ref persona) = persona {
1492 tracing::info!("Using persona '{}' for route generation", persona.name);
1493 }
1494 OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1495 };
1496
1497 let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1499 .unwrap_or_else(|_| "/app/fixtures".to_string());
1500 let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1501 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1502 .unwrap_or(true); if custom_fixtures_enabled {
1505 use mockforge_core::CustomFixtureLoader;
1506 use std::path::PathBuf;
1507 use std::sync::Arc;
1508
1509 let fixtures_path = PathBuf::from(&fixtures_dir);
1510 let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1511
1512 if let Err(e) = custom_loader.load_fixtures().await {
1513 tracing::warn!("Failed to load custom fixtures: {}", e);
1514 } else {
1515 tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1516 registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1517 }
1518 }
1519
1520 if registry
1521 .routes()
1522 .iter()
1523 .any(|route| route.method == "GET" && route.path == "/health")
1524 {
1525 include_default_health = false;
1526 }
1527 let spec_router = if let Some(ref mockai_instance) = _mockai {
1529 tracing::debug!("Building router with MockAI support");
1530 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1531 } else {
1532 registry.build_router()
1533 };
1534 app = app.merge(spec_router);
1535 }
1536 Err(e) => {
1537 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1538 }
1539 }
1540 }
1541
1542 fn expand_templates_in_json(
1544 value: &serde_json::Value,
1545 context: &mockforge_core::ai_response::RequestContext,
1546 ) -> serde_json::Value {
1547 use mockforge_core::ai_response::expand_prompt_template;
1548 use serde_json::Value;
1549
1550 match value {
1551 Value::String(s) => {
1552 let normalized = s
1554 .replace("{{request.query.", "{{query.")
1555 .replace("{{request.path.", "{{path.")
1556 .replace("{{request.headers.", "{{headers.")
1557 .replace("{{request.body.", "{{body.")
1558 .replace("{{request.method}}", "{{method}}")
1559 .replace("{{request.path}}", "{{path}}");
1560
1561 let (template_part, default_value) = if normalized.contains("||") {
1564 if let Some(open_idx) = normalized.find("{{") {
1568 if let Some(close_idx) = normalized[open_idx..].find("}}") {
1569 let template_block = &normalized[open_idx..open_idx + close_idx + 2];
1570 if let Some(pipe_idx) = template_block.find("||") {
1571 let before_pipe = &template_block[..pipe_idx].trim();
1573 let after_pipe = &template_block[pipe_idx + 2..].trim();
1574
1575 let template_var = before_pipe.trim_start_matches("{{").trim();
1577 let replacement = format!("{{{{{}}}}}}}", template_var);
1579 let template = normalized.replace(template_block, &replacement);
1580
1581 let mut default =
1583 after_pipe.trim_end_matches("}}").trim().to_string();
1584 default = default
1586 .trim_matches('"')
1587 .trim_matches('\'')
1588 .trim_matches('\\')
1589 .to_string();
1590 default = default.trim().to_string();
1591
1592 (template, Some(default))
1593 } else {
1594 (normalized, None)
1595 }
1596 } else {
1597 (normalized, None)
1598 }
1599 } else {
1600 (normalized, None)
1601 }
1602 } else {
1603 (normalized, None)
1604 };
1605
1606 let mut expanded = expand_prompt_template(&template_part, context);
1608
1609 let final_expanded = if (expanded.contains("{{query.")
1612 || expanded.contains("{{path.")
1613 || expanded.contains("{{headers."))
1614 && default_value.is_some()
1615 {
1616 default_value.unwrap()
1617 } else {
1618 while expanded.ends_with('}') && !expanded.ends_with("}}") {
1621 expanded.pop();
1622 }
1623 expanded
1624 };
1625
1626 Value::String(final_expanded)
1627 }
1628 Value::Array(arr) => {
1629 Value::Array(arr.iter().map(|v| expand_templates_in_json(v, context)).collect())
1630 }
1631 Value::Object(obj) => {
1632 let mut new_obj = serde_json::Map::new();
1633 for (k, v) in obj {
1634 new_obj.insert(k.clone(), expand_templates_in_json(v, context));
1635 }
1636 Value::Object(new_obj)
1637 }
1638 _ => value.clone(),
1639 }
1640 }
1641
1642 if let Some(route_configs) = route_configs {
1644 use axum::http::StatusCode;
1645 use axum::response::IntoResponse;
1646
1647 if !route_configs.is_empty() {
1648 info!("Registering {} custom route(s) from config", route_configs.len());
1649 }
1650
1651 for route_config in route_configs {
1652 let status = route_config.response.status;
1653 let body = route_config.response.body.clone();
1654 let headers = route_config.response.headers.clone();
1655 let path = route_config.path.clone();
1656 let method = route_config.method.clone();
1657 let latency_config = route_config.latency.clone();
1658
1659 let expected_method = method.to_uppercase();
1664 app = app.route(
1665 &path,
1666 axum::routing::any(move |req: axum::http::Request<axum::body::Body>| {
1667 let body = body.clone();
1668 let headers = headers.clone();
1669 let expand = template_expand;
1670 let latency = latency_config.clone();
1671 let expected = expected_method.clone();
1672 let status_code = status;
1673
1674 async move {
1675 if req.method().as_str() != expected.as_str() {
1677 return axum::response::Response::builder()
1679 .status(axum::http::StatusCode::METHOD_NOT_ALLOWED)
1680 .header("Allow", &expected)
1681 .body(axum::body::Body::empty())
1682 .unwrap()
1683 .into_response();
1684 }
1685
1686 let delay_ms = if let Some(ref lat) = latency {
1689 if lat.enabled {
1690 use rand::{rng, Rng};
1691
1692 let mut rng = rng();
1694 let roll: f64 = rng.random();
1695
1696 if roll < lat.probability {
1697 if let Some(fixed) = lat.fixed_delay_ms {
1698 let jitter =
1700 (fixed as f64 * lat.jitter_percent / 100.0) as u64;
1701 let jitter_amount = if jitter > 0 {
1702 rng.random_range(0..=jitter)
1703 } else {
1704 0
1705 };
1706 Some(fixed + jitter_amount)
1707 } else if let Some((min, max)) = lat.random_delay_range_ms {
1708 Some(rng.random_range(min..=max))
1710 } else {
1711 Some(0)
1713 }
1714 } else {
1715 None
1716 }
1717 } else {
1718 None
1719 }
1720 } else {
1721 None
1722 };
1723
1724 if let Some(delay) = delay_ms {
1726 if delay > 0 {
1727 use tokio::time::{sleep, Duration};
1728 sleep(Duration::from_millis(delay)).await;
1729 }
1730 }
1731
1732 let mut body_value = body.unwrap_or(serde_json::json!({}));
1734
1735 if expand {
1737 use mockforge_core::ai_response::RequestContext;
1738 use serde_json::Value;
1739 use std::collections::HashMap;
1740
1741 let method = req.method().to_string();
1743 let path = req.uri().path().to_string();
1744
1745 let query_params: HashMap<String, Value> = req
1747 .uri()
1748 .query()
1749 .map(|q| {
1750 url::form_urlencoded::parse(q.as_bytes())
1751 .into_owned()
1752 .map(|(k, v)| (k, Value::String(v)))
1753 .collect()
1754 })
1755 .unwrap_or_default();
1756
1757 let request_headers: HashMap<String, Value> =
1759 req.headers()
1760 .iter()
1761 .filter_map(|(name, value)| {
1762 value.to_str().ok().map(|v| {
1763 (name.to_string(), Value::String(v.to_string()))
1764 })
1765 })
1766 .collect();
1767
1768 let context = RequestContext::new(method.clone(), path.clone())
1773 .with_query_params(query_params)
1774 .with_headers(request_headers);
1775
1776 body_value = expand_templates_in_json(&body_value, &context);
1778 }
1779
1780 let mut response = axum::Json(body_value).into_response();
1781
1782 *response.status_mut() =
1784 StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
1785
1786 for (key, value) in headers {
1788 if let Ok(header_name) =
1789 axum::http::HeaderName::from_bytes(key.as_bytes())
1790 {
1791 if let Ok(header_value) = axum::http::HeaderValue::from_str(&value)
1792 {
1793 response.headers_mut().insert(header_name, header_value);
1794 }
1795 }
1796 }
1797
1798 response
1799 }
1800 }),
1801 );
1802
1803 debug!("Registered route: {} {}", method, path);
1804 }
1805 }
1806
1807 if let Some(health) = health_manager {
1809 app = app.merge(health::health_router(health));
1811 info!(
1812 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1813 );
1814 } else if include_default_health {
1815 app = app.route(
1817 "/health",
1818 axum::routing::get(|| async {
1819 use mockforge_core::server_utils::health::HealthStatus;
1820 {
1821 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1823 Ok(value) => axum::Json(value),
1824 Err(e) => {
1825 tracing::error!("Failed to serialize health status: {}", e);
1827 axum::Json(serde_json::json!({
1828 "status": "healthy",
1829 "service": "mockforge-http",
1830 "uptime_seconds": 0
1831 }))
1832 }
1833 }
1834 }
1835 }),
1836 );
1837 }
1838
1839 app = app.merge(sse::sse_router());
1840 app = app.merge(file_server::file_serving_router());
1842
1843 let spec_path_clone = spec_path.clone();
1845 let mut management_state = ManagementState::new(None, spec_path_clone, 3000); use std::sync::Arc;
1849 let ws_state = WsManagementState::new();
1850 let ws_broadcast = Arc::new(ws_state.tx.clone());
1851 let management_state = management_state.with_ws_broadcast(ws_broadcast);
1852
1853 let management_state = if let Some(proxy_cfg) = proxy_config {
1855 use tokio::sync::RwLock;
1856 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
1857 management_state.with_proxy_config(proxy_config_arc)
1858 } else {
1859 management_state
1860 };
1861
1862 #[cfg(feature = "smtp")]
1863 let management_state = {
1864 if let Some(smtp_reg) = smtp_registry {
1865 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
1866 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
1867 Err(e) => {
1868 error!(
1869 "Invalid SMTP registry type passed to HTTP management state: {:?}",
1870 e.type_id()
1871 );
1872 management_state
1873 }
1874 }
1875 } else {
1876 management_state
1877 }
1878 };
1879 #[cfg(not(feature = "smtp"))]
1880 let management_state = {
1881 let _ = smtp_registry;
1882 management_state
1883 };
1884 #[cfg(feature = "mqtt")]
1885 let management_state = {
1886 if let Some(broker) = mqtt_broker {
1887 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
1888 Ok(broker) => management_state.with_mqtt_broker(broker),
1889 Err(e) => {
1890 error!(
1891 "Invalid MQTT broker passed to HTTP management state: {:?}",
1892 e.type_id()
1893 );
1894 management_state
1895 }
1896 }
1897 } else {
1898 management_state
1899 }
1900 };
1901 #[cfg(not(feature = "mqtt"))]
1902 let management_state = {
1903 let _ = mqtt_broker;
1904 management_state
1905 };
1906 app = app.nest("/__mockforge/api", management_router(management_state));
1907
1908 app = app.merge(verification_router());
1910
1911 use crate::auth::oidc::oidc_router;
1913 app = app.merge(oidc_router());
1914
1915 {
1917 use mockforge_core::security::get_global_access_review_service;
1918 if let Some(service) = get_global_access_review_service().await {
1919 use crate::handlers::access_review::{access_review_router, AccessReviewState};
1920 let review_state = AccessReviewState { service };
1921 app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
1922 debug!("Access review API mounted at /api/v1/security/access-reviews");
1923 }
1924 }
1925
1926 {
1928 use mockforge_core::security::get_global_privileged_access_manager;
1929 if let Some(manager) = get_global_privileged_access_manager().await {
1930 use crate::handlers::privileged_access::{
1931 privileged_access_router, PrivilegedAccessState,
1932 };
1933 let privileged_state = PrivilegedAccessState { manager };
1934 app = app.nest(
1935 "/api/v1/security/privileged-access",
1936 privileged_access_router(privileged_state),
1937 );
1938 debug!("Privileged access API mounted at /api/v1/security/privileged-access");
1939 }
1940 }
1941
1942 {
1944 use mockforge_core::security::get_global_change_management_engine;
1945 if let Some(engine) = get_global_change_management_engine().await {
1946 use crate::handlers::change_management::{
1947 change_management_router, ChangeManagementState,
1948 };
1949 let change_state = ChangeManagementState { engine };
1950 app = app.nest("/api/v1/change-management", change_management_router(change_state));
1951 debug!("Change management API mounted at /api/v1/change-management");
1952 }
1953 }
1954
1955 {
1957 use mockforge_core::security::get_global_risk_assessment_engine;
1958 if let Some(engine) = get_global_risk_assessment_engine().await {
1959 use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
1960 let risk_state = RiskAssessmentState { engine };
1961 app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
1962 debug!("Risk assessment API mounted at /api/v1/security/risks");
1963 }
1964 }
1965
1966 {
1968 use crate::auth::token_lifecycle::TokenLifecycleManager;
1969 use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
1970 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1971 let lifecycle_state = TokenLifecycleState {
1972 manager: lifecycle_manager,
1973 };
1974 app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1975 debug!("Token lifecycle API mounted at /api/v1/auth");
1976 }
1977
1978 {
1980 use crate::auth::oidc::{load_oidc_state, OidcState};
1981 use crate::auth::token_lifecycle::TokenLifecycleManager;
1982 use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1983 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1985 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1986 let oauth2_state = OAuth2ServerState {
1987 oidc_state,
1988 lifecycle_manager,
1989 auth_codes: Arc::new(RwLock::new(HashMap::new())),
1990 };
1991 app = app.merge(oauth2_server_router(oauth2_state));
1992 debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1993 }
1994
1995 {
1997 use crate::auth::oidc::{load_oidc_state, OidcState};
1998 use crate::auth::risk_engine::RiskEngine;
1999 use crate::auth::token_lifecycle::TokenLifecycleManager;
2000 use crate::handlers::consent::{consent_router, ConsentState};
2001 use crate::handlers::oauth2_server::OAuth2ServerState;
2002 let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2004 let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2005 let oauth2_state = OAuth2ServerState {
2006 oidc_state: oidc_state.clone(),
2007 lifecycle_manager: lifecycle_manager.clone(),
2008 auth_codes: Arc::new(RwLock::new(HashMap::new())),
2009 };
2010 let risk_engine = Arc::new(RiskEngine::default());
2011 let consent_state = ConsentState {
2012 oauth2_state,
2013 risk_engine,
2014 };
2015 app = app.merge(consent_router(consent_state));
2016 debug!("Consent screen endpoints mounted at /consent");
2017 }
2018
2019 {
2021 use crate::auth::risk_engine::RiskEngine;
2022 use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2023 let risk_engine = Arc::new(RiskEngine::default());
2024 let risk_state = RiskSimulationState { risk_engine };
2025 app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2026 debug!("Risk simulation API mounted at /api/v1/auth/risk");
2027 }
2028
2029 let database = {
2031 use crate::database::Database;
2032 let database_url = std::env::var("DATABASE_URL").ok();
2033 match Database::connect_optional(database_url.as_deref()).await {
2034 Ok(db) => {
2035 if db.is_connected() {
2036 if let Err(e) = db.migrate_if_connected().await {
2038 warn!("Failed to run database migrations: {}", e);
2039 } else {
2040 info!("Database connected and migrations applied");
2041 }
2042 }
2043 Some(db)
2044 }
2045 Err(e) => {
2046 warn!("Failed to connect to database: {}. Continuing without database support.", e);
2047 None
2048 }
2049 }
2050 };
2051
2052 {
2054 use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2055 use crate::middleware::drift_tracking::DriftTrackingState;
2056 use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2057 use mockforge_core::consumer_contracts::{ConsumerBreakingChangeDetector, UsageRecorder};
2058 use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2059 use mockforge_core::incidents::{IncidentManager, IncidentStore};
2060 use std::sync::Arc;
2061
2062 let drift_config = DriftBudgetConfig::default();
2064 let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2065
2066 let incident_store = Arc::new(IncidentStore::default());
2068 let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2069
2070 let usage_recorder = Arc::new(UsageRecorder::default());
2072 let consumer_detector =
2073 Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2074
2075 let diff_analyzer = if drift_config.enabled {
2077 match ContractDiffAnalyzer::new(
2078 mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2079 ) {
2080 Ok(analyzer) => Some(Arc::new(analyzer)),
2081 Err(e) => {
2082 warn!("Failed to create contract diff analyzer: {}", e);
2083 None
2084 }
2085 }
2086 } else {
2087 None
2088 };
2089
2090 let spec = if let Some(ref spec_path) = spec_path {
2093 match mockforge_core::openapi::OpenApiSpec::from_file(spec_path).await {
2094 Ok(s) => Some(Arc::new(s)),
2095 Err(e) => {
2096 debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2097 None
2098 }
2099 }
2100 } else {
2101 None
2102 };
2103
2104 let drift_tracking_state = DriftTrackingState {
2106 diff_analyzer,
2107 spec,
2108 drift_engine: drift_engine.clone(),
2109 incident_manager: incident_manager.clone(),
2110 usage_recorder,
2111 consumer_detector,
2112 enabled: drift_config.enabled,
2113 };
2114
2115 app = app.layer(axum::middleware::from_fn(crate::middleware::buffer_response_middleware));
2117
2118 let drift_tracking_state_clone = drift_tracking_state.clone();
2121 app = app.layer(axum::middleware::from_fn(
2122 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2123 let state = drift_tracking_state_clone.clone();
2124 async move {
2125 if req
2127 .extensions()
2128 .get::<crate::middleware::drift_tracking::DriftTrackingState>()
2129 .is_none()
2130 {
2131 req.extensions_mut().insert(state);
2132 }
2133 crate::middleware::drift_tracking::drift_tracking_middleware_with_extensions(
2135 req, next,
2136 )
2137 .await
2138 }
2139 },
2140 ));
2141
2142 let drift_state = DriftBudgetState {
2143 engine: drift_engine,
2144 incident_manager,
2145 gitops_handler: None, };
2147
2148 app = app.merge(drift_budget_router(drift_state));
2149 debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2150 }
2151
2152 {
2154 use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2155 use std::path::PathBuf;
2156
2157 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2159 .ok()
2160 .map(PathBuf::from)
2161 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2162
2163 let bc_middleware_state = if let Some(path) = db_path {
2164 BehavioralCloningMiddlewareState::with_database_path(path)
2165 } else {
2166 BehavioralCloningMiddlewareState::new()
2167 };
2168
2169 let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2171 .ok()
2172 .and_then(|v| v.parse::<bool>().ok())
2173 .unwrap_or(false);
2174
2175 if enabled {
2176 let bc_state_clone = bc_middleware_state.clone();
2177 app = app.layer(axum::middleware::from_fn(
2178 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2179 let state = bc_state_clone.clone();
2180 async move {
2181 if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2183 req.extensions_mut().insert(state);
2184 }
2185 crate::middleware::behavioral_cloning::behavioral_cloning_middleware(
2187 req, next,
2188 )
2189 .await
2190 }
2191 },
2192 ));
2193 debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2194 }
2195 }
2196
2197 {
2199 use crate::handlers::consumer_contracts::{
2200 consumer_contracts_router, ConsumerContractsState,
2201 };
2202 use mockforge_core::consumer_contracts::{
2203 ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2204 };
2205 use std::sync::Arc;
2206
2207 let registry = Arc::new(ConsumerRegistry::default());
2209
2210 let usage_recorder = Arc::new(UsageRecorder::default());
2212
2213 let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2215
2216 let consumer_state = ConsumerContractsState {
2217 registry,
2218 usage_recorder,
2219 detector,
2220 };
2221
2222 app = app.merge(consumer_contracts_router(consumer_state));
2223 debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2224 }
2225
2226 {
2228 use crate::handlers::behavioral_cloning::{
2229 behavioral_cloning_router, BehavioralCloningState,
2230 };
2231 use std::path::PathBuf;
2232
2233 let db_path = std::env::var("RECORDER_DATABASE_PATH")
2235 .ok()
2236 .map(PathBuf::from)
2237 .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2238
2239 let bc_state = if let Some(path) = db_path {
2240 BehavioralCloningState::with_database_path(path)
2241 } else {
2242 BehavioralCloningState::new()
2243 };
2244
2245 app = app.merge(behavioral_cloning_router(bc_state));
2246 debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2247 }
2248
2249 {
2251 use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2252 use crate::handlers::consistency::{consistency_router, ConsistencyState};
2253 use mockforge_core::consistency::ConsistencyEngine;
2254 use std::sync::Arc;
2255
2256 let consistency_engine = Arc::new(ConsistencyEngine::new());
2258
2259 let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2261 consistency_engine.register_adapter(http_adapter.clone()).await;
2262
2263 let consistency_state = ConsistencyState {
2265 engine: consistency_engine.clone(),
2266 };
2267
2268 let consistency_middleware_state = ConsistencyMiddlewareState {
2270 engine: consistency_engine.clone(),
2271 adapter: http_adapter,
2272 };
2273
2274 let consistency_middleware_state_clone = consistency_middleware_state.clone();
2276 app = app.layer(axum::middleware::from_fn(
2277 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2278 let state = consistency_middleware_state_clone.clone();
2279 async move {
2280 if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2282 req.extensions_mut().insert(state);
2283 }
2284 crate::consistency::middleware::consistency_middleware(req, next).await
2286 }
2287 },
2288 ));
2289
2290 app = app.merge(consistency_router(consistency_state));
2292 debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2293
2294 {
2296 use crate::handlers::fidelity::{fidelity_router, FidelityState};
2297 let fidelity_state = FidelityState::new();
2298 app = app.merge(fidelity_router(fidelity_state));
2299 debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
2300 }
2301
2302 {
2304 use crate::handlers::scenario_studio::{scenario_studio_router, ScenarioStudioState};
2305 let scenario_studio_state = ScenarioStudioState::new();
2306 app = app.merge(scenario_studio_router(scenario_studio_state));
2307 debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
2308 }
2309
2310 {
2312 use crate::handlers::snapshots::{snapshot_router, SnapshotState};
2313 use mockforge_core::snapshots::SnapshotManager;
2314 use std::path::PathBuf;
2315
2316 let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
2317 let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
2318
2319 let snapshot_state = SnapshotState {
2320 manager: snapshot_manager,
2321 consistency_engine: Some(consistency_engine.clone()),
2322 };
2323
2324 app = app.merge(snapshot_router(snapshot_state));
2325 debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
2326
2327 {
2329 use crate::handlers::xray::{xray_router, XRayState};
2330 let xray_state = XRayState {
2331 engine: consistency_engine.clone(),
2332 };
2333 app = app.merge(xray_router(xray_state));
2334 debug!("X-Ray API endpoints mounted at /api/v1/xray");
2335 }
2336 }
2337
2338 {
2340 use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
2341 use crate::middleware::ab_testing::ab_testing_middleware;
2342
2343 let ab_testing_state = ABTestingState::new();
2344
2345 let ab_testing_state_clone = ab_testing_state.clone();
2347 app = app.layer(axum::middleware::from_fn(
2348 move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2349 let state = ab_testing_state_clone.clone();
2350 async move {
2351 if req.extensions().get::<ABTestingState>().is_none() {
2353 req.extensions_mut().insert(state);
2354 }
2355 ab_testing_middleware(req, next).await
2357 }
2358 },
2359 ));
2360
2361 app = app.merge(ab_testing_router(ab_testing_state));
2363 debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
2364 }
2365 }
2366
2367 {
2369 use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
2370 use mockforge_core::pr_generation::{PRGenerator, PRProvider};
2371 use std::sync::Arc;
2372
2373 let pr_config = mockforge_core::pr_generation::PRGenerationConfig::from_env();
2375
2376 let generator = if pr_config.enabled && pr_config.token.is_some() {
2377 let token = pr_config.token.as_ref().unwrap().clone();
2378 let generator = match pr_config.provider {
2379 PRProvider::GitHub => PRGenerator::new_github(
2380 pr_config.owner.clone(),
2381 pr_config.repo.clone(),
2382 token,
2383 pr_config.base_branch.clone(),
2384 ),
2385 PRProvider::GitLab => PRGenerator::new_gitlab(
2386 pr_config.owner.clone(),
2387 pr_config.repo.clone(),
2388 token,
2389 pr_config.base_branch.clone(),
2390 ),
2391 };
2392 Some(Arc::new(generator))
2393 } else {
2394 None
2395 };
2396
2397 let pr_state = PRGenerationState {
2398 generator: generator.clone(),
2399 };
2400
2401 app = app.merge(pr_generation_router(pr_state));
2402 if generator.is_some() {
2403 debug!(
2404 "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
2405 pr_config.provider
2406 );
2407 } else {
2408 debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
2409 }
2410 }
2411
2412 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
2414
2415 if let Some(mt_config) = multi_tenant_config {
2417 if mt_config.enabled {
2418 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
2419 use std::sync::Arc;
2420
2421 info!(
2422 "Multi-tenant mode enabled with {} routing strategy",
2423 match mt_config.routing_strategy {
2424 mockforge_core::RoutingStrategy::Path => "path-based",
2425 mockforge_core::RoutingStrategy::Port => "port-based",
2426 mockforge_core::RoutingStrategy::Both => "hybrid",
2427 }
2428 );
2429
2430 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
2432
2433 let default_workspace =
2435 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
2436 if let Err(e) =
2437 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
2438 {
2439 warn!("Failed to register default workspace: {}", e);
2440 } else {
2441 info!("Registered default workspace: '{}'", mt_config.default_workspace);
2442 }
2443
2444 let registry = Arc::new(registry);
2446
2447 let _workspace_router = WorkspaceRouter::new(registry);
2449 info!("Workspace routing middleware initialized for HTTP server");
2450 }
2451 }
2452
2453 let mut final_cors_config = cors_config;
2455 let mut production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>> =
2456 None;
2457 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
2459 let mut rate_limit_config = crate::middleware::RateLimitConfig {
2460 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
2461 .ok()
2462 .and_then(|v| v.parse().ok())
2463 .unwrap_or(1000),
2464 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
2465 .ok()
2466 .and_then(|v| v.parse().ok())
2467 .unwrap_or(2000),
2468 per_ip: true,
2469 per_endpoint: false,
2470 };
2471
2472 if let Some(deploy_config) = &deceptive_deploy_config {
2473 if deploy_config.enabled {
2474 info!("Deceptive deploy mode enabled - applying production-like configuration");
2475
2476 if let Some(prod_cors) = &deploy_config.cors {
2478 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
2479 enabled: true,
2480 allowed_origins: prod_cors.allowed_origins.clone(),
2481 allowed_methods: prod_cors.allowed_methods.clone(),
2482 allowed_headers: prod_cors.allowed_headers.clone(),
2483 allow_credentials: prod_cors.allow_credentials,
2484 });
2485 info!("Applied production-like CORS configuration");
2486 }
2487
2488 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
2490 rate_limit_config = crate::middleware::RateLimitConfig {
2491 requests_per_minute: prod_rate_limit.requests_per_minute,
2492 burst: prod_rate_limit.burst,
2493 per_ip: prod_rate_limit.per_ip,
2494 per_endpoint: false,
2495 };
2496 info!(
2497 "Applied production-like rate limiting: {} req/min, burst: {}",
2498 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
2499 );
2500 }
2501
2502 if !deploy_config.headers.is_empty() {
2504 let headers_map: std::collections::HashMap<String, String> =
2505 deploy_config.headers.clone();
2506 production_headers = Some(std::sync::Arc::new(headers_map));
2507 info!("Configured {} production headers", deploy_config.headers.len());
2508 }
2509
2510 if let Some(prod_oauth) = &deploy_config.oauth {
2512 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
2513 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
2514 oauth2: Some(oauth2_config),
2515 ..Default::default()
2516 });
2517 info!("Applied production-like OAuth configuration for deceptive deploy");
2518 }
2519 }
2520 }
2521
2522 let rate_limiter =
2524 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
2525
2526 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
2527
2528 if let Some(headers) = production_headers.clone() {
2530 state = state.with_production_headers(headers);
2531 }
2532
2533 app = app.layer(from_fn_with_state(state.clone(), crate::middleware::rate_limit_middleware));
2535
2536 if state.production_headers.is_some() {
2538 app = app.layer(from_fn_with_state(
2539 state.clone(),
2540 crate::middleware::production_headers_middleware,
2541 ));
2542 }
2543
2544 if let Some(auth_config) = deceptive_deploy_auth_config {
2546 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
2547 use std::collections::HashMap;
2548 use std::sync::Arc;
2549 use tokio::sync::RwLock;
2550
2551 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
2553 match create_oauth2_client(oauth2_config) {
2554 Ok(client) => Some(client),
2555 Err(e) => {
2556 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
2557 None
2558 }
2559 }
2560 } else {
2561 None
2562 };
2563
2564 let auth_state = AuthState {
2566 config: auth_config,
2567 spec: None, oauth2_client,
2569 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
2570 };
2571
2572 app = app.layer(axum::middleware::from_fn_with_state(auth_state, auth_middleware));
2574 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
2575 }
2576
2577 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
2580
2581 app = apply_cors_middleware(app, final_cors_config);
2583
2584 app
2585}
2586
2587#[test]
2591fn test_route_info_clone() {
2592 let route = RouteInfo {
2593 method: "POST".to_string(),
2594 path: "/users".to_string(),
2595 operation_id: Some("createUser".to_string()),
2596 summary: None,
2597 description: None,
2598 parameters: vec![],
2599 };
2600
2601 let cloned = route.clone();
2602 assert_eq!(route.method, cloned.method);
2603 assert_eq!(route.path, cloned.path);
2604 assert_eq!(route.operation_id, cloned.operation_id);
2605}
2606
2607#[test]
2608fn test_http_server_state_new() {
2609 let state = HttpServerState::new();
2610 assert_eq!(state.routes.len(), 0);
2611}
2612
2613#[test]
2614fn test_http_server_state_with_routes() {
2615 let routes = vec![
2616 RouteInfo {
2617 method: "GET".to_string(),
2618 path: "/users".to_string(),
2619 operation_id: Some("getUsers".to_string()),
2620 summary: None,
2621 description: None,
2622 parameters: vec![],
2623 },
2624 RouteInfo {
2625 method: "POST".to_string(),
2626 path: "/users".to_string(),
2627 operation_id: Some("createUser".to_string()),
2628 summary: None,
2629 description: None,
2630 parameters: vec![],
2631 },
2632 ];
2633
2634 let state = HttpServerState::with_routes(routes.clone());
2635 assert_eq!(state.routes.len(), 2);
2636 assert_eq!(state.routes[0].method, "GET");
2637 assert_eq!(state.routes[1].method, "POST");
2638}
2639
2640#[test]
2641fn test_http_server_state_clone() {
2642 let routes = vec![RouteInfo {
2643 method: "GET".to_string(),
2644 path: "/test".to_string(),
2645 operation_id: None,
2646 summary: None,
2647 description: None,
2648 parameters: vec![],
2649 }];
2650
2651 let state = HttpServerState::with_routes(routes);
2652 let cloned = state.clone();
2653
2654 assert_eq!(state.routes.len(), cloned.routes.len());
2655 assert_eq!(state.routes[0].method, cloned.routes[0].method);
2656}
2657
2658#[tokio::test]
2659async fn test_build_router_without_openapi() {
2660 let _router = build_router(None, None, None).await;
2661 }
2663
2664#[tokio::test]
2665async fn test_build_router_with_nonexistent_spec() {
2666 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
2667 }
2669
2670#[tokio::test]
2671async fn test_build_router_with_auth_and_latency() {
2672 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
2673 }
2675
2676#[tokio::test]
2677async fn test_build_router_with_latency() {
2678 let _router = build_router_with_latency(None, None, None).await;
2679 }
2681
2682#[tokio::test]
2683async fn test_build_router_with_auth() {
2684 let _router = build_router_with_auth(None, None, None).await;
2685 }
2687
2688#[tokio::test]
2689async fn test_build_router_with_chains() {
2690 let _router = build_router_with_chains(None, None, None).await;
2691 }
2693
2694#[test]
2695fn test_route_info_with_all_fields() {
2696 let route = RouteInfo {
2697 method: "PUT".to_string(),
2698 path: "/users/{id}".to_string(),
2699 operation_id: Some("updateUser".to_string()),
2700 summary: Some("Update user".to_string()),
2701 description: Some("Updates an existing user".to_string()),
2702 parameters: vec!["id".to_string(), "body".to_string()],
2703 };
2704
2705 assert!(route.operation_id.is_some());
2706 assert!(route.summary.is_some());
2707 assert!(route.description.is_some());
2708 assert_eq!(route.parameters.len(), 2);
2709}
2710
2711#[test]
2712fn test_route_info_with_minimal_fields() {
2713 let route = RouteInfo {
2714 method: "DELETE".to_string(),
2715 path: "/users/{id}".to_string(),
2716 operation_id: None,
2717 summary: None,
2718 description: None,
2719 parameters: vec![],
2720 };
2721
2722 assert!(route.operation_id.is_none());
2723 assert!(route.summary.is_none());
2724 assert!(route.description.is_none());
2725 assert_eq!(route.parameters.len(), 0);
2726}
2727
2728#[test]
2729fn test_http_server_state_empty_routes() {
2730 let state = HttpServerState::with_routes(vec![]);
2731 assert_eq!(state.routes.len(), 0);
2732}
2733
2734#[test]
2735fn test_http_server_state_multiple_routes() {
2736 let routes = vec![
2737 RouteInfo {
2738 method: "GET".to_string(),
2739 path: "/users".to_string(),
2740 operation_id: Some("listUsers".to_string()),
2741 summary: Some("List all users".to_string()),
2742 description: None,
2743 parameters: vec![],
2744 },
2745 RouteInfo {
2746 method: "GET".to_string(),
2747 path: "/users/{id}".to_string(),
2748 operation_id: Some("getUser".to_string()),
2749 summary: Some("Get a user".to_string()),
2750 description: None,
2751 parameters: vec!["id".to_string()],
2752 },
2753 RouteInfo {
2754 method: "POST".to_string(),
2755 path: "/users".to_string(),
2756 operation_id: Some("createUser".to_string()),
2757 summary: Some("Create a user".to_string()),
2758 description: None,
2759 parameters: vec!["body".to_string()],
2760 },
2761 ];
2762
2763 let state = HttpServerState::with_routes(routes);
2764 assert_eq!(state.routes.len(), 3);
2765
2766 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
2768 assert!(methods.contains(&"GET"));
2769 assert!(methods.contains(&"POST"));
2770}
2771
2772#[test]
2773fn test_http_server_state_with_rate_limiter() {
2774 use std::sync::Arc;
2775
2776 let config = crate::middleware::RateLimitConfig::default();
2777 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
2778
2779 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
2780
2781 assert!(state.rate_limiter.is_some());
2782 assert_eq!(state.routes.len(), 0);
2783}
2784
2785#[tokio::test]
2786async fn test_build_router_includes_rate_limiter() {
2787 let _router = build_router(None, None, None).await;
2788 }