1pub mod ai_handler;
166pub mod auth;
167pub mod chain_handlers;
168pub mod contract_diff_middleware;
170pub mod coverage;
171pub mod file_generator;
173pub mod file_server;
175pub mod health;
177pub mod http_tracing_middleware;
178pub mod latency_profiles;
180pub mod management;
182pub mod management_ws;
184pub mod metrics_middleware;
185pub mod middleware;
186pub mod op_middleware;
187pub mod proxy_server;
189pub mod quick_mock;
191pub mod rag_ai_generator;
193pub mod replay_listing;
195pub mod request_logging;
196pub mod spec_import;
198pub mod sse;
200pub mod state_machine_api;
202pub mod tls;
204pub mod token_response;
206pub mod ui_builder;
208pub mod verification;
210
211pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
213pub use health::{HealthManager, ServiceStatus};
215
216pub use management::{
218 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
219 ServerConfig, ServerStats,
220};
221
222pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
224
225pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
227
228pub use verification::verification_router;
230
231pub use metrics_middleware::collect_http_metrics;
233
234pub use http_tracing_middleware::http_tracing_middleware;
236
237pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
239
240use axum::middleware::from_fn_with_state;
241use axum::{extract::State, response::Json, Router};
242use mockforge_core::failure_injection::{FailureConfig, FailureInjector};
243use mockforge_core::latency::LatencyInjector;
244use mockforge_core::openapi::OpenApiSpec;
245use mockforge_core::openapi_routes::OpenApiRouteRegistry;
246use mockforge_core::openapi_routes::ValidationOptions;
247use tower_http::cors::{Any, CorsLayer};
248
249use mockforge_core::LatencyProfile;
250#[cfg(feature = "data-faker")]
251use mockforge_data::provider::register_core_faker_provider;
252use std::collections::HashMap;
253use std::ffi::OsStr;
254use std::path::Path;
255use tokio::fs;
256use tokio::sync::RwLock;
257use tracing::*;
258
259#[derive(Clone)]
261pub struct RouteInfo {
262 pub method: String,
264 pub path: String,
266 pub operation_id: Option<String>,
268 pub summary: Option<String>,
270 pub description: Option<String>,
272 pub parameters: Vec<String>,
274}
275
276#[derive(Clone)]
278pub struct HttpServerState {
279 pub routes: Vec<RouteInfo>,
281 pub rate_limiter: Option<std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>>,
283 pub production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>>,
285}
286
287impl Default for HttpServerState {
288 fn default() -> Self {
289 Self::new()
290 }
291}
292
293impl HttpServerState {
294 pub fn new() -> Self {
296 Self {
297 routes: Vec::new(),
298 rate_limiter: None,
299 production_headers: None,
300 }
301 }
302
303 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
305 Self {
306 routes,
307 rate_limiter: None,
308 production_headers: None,
309 }
310 }
311
312 pub fn with_rate_limiter(
314 mut self,
315 rate_limiter: std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>,
316 ) -> Self {
317 self.rate_limiter = Some(rate_limiter);
318 self
319 }
320
321 pub fn with_production_headers(
323 mut self,
324 headers: std::sync::Arc<std::collections::HashMap<String, String>>,
325 ) -> Self {
326 self.production_headers = Some(headers);
327 self
328 }
329}
330
331async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
333 let route_info: Vec<serde_json::Value> = state
334 .routes
335 .iter()
336 .map(|route| {
337 serde_json::json!({
338 "method": route.method,
339 "path": route.path,
340 "operation_id": route.operation_id,
341 "summary": route.summary,
342 "description": route.description,
343 "parameters": route.parameters
344 })
345 })
346 .collect();
347
348 Json(serde_json::json!({
349 "routes": route_info,
350 "total": state.routes.len()
351 }))
352}
353
354pub async fn build_router(
356 spec_path: Option<String>,
357 options: Option<ValidationOptions>,
358 failure_config: Option<FailureConfig>,
359) -> Router {
360 build_router_with_multi_tenant(
361 spec_path,
362 options,
363 failure_config,
364 None,
365 None,
366 None,
367 None,
368 None,
369 None,
370 None,
371 )
372 .await
373}
374
375fn apply_cors_middleware(
377 app: Router,
378 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
379) -> Router {
380 use http::Method;
381 use tower_http::cors::AllowOrigin;
382
383 if let Some(config) = cors_config {
384 if !config.enabled {
385 return app;
386 }
387
388 let mut cors_layer = CorsLayer::new();
389
390 if config.allowed_origins.contains(&"*".to_string()) {
392 cors_layer = cors_layer.allow_origin(Any);
393 } else if !config.allowed_origins.is_empty() {
394 let origins: Vec<_> = config
396 .allowed_origins
397 .iter()
398 .filter_map(|origin| {
399 origin.parse::<http::HeaderValue>().ok().map(|hv| AllowOrigin::exact(hv))
400 })
401 .collect();
402
403 if origins.is_empty() {
404 warn!("No valid CORS origins configured, using permissive CORS");
406 cors_layer = cors_layer.allow_origin(Any);
407 } else {
408 if origins.len() == 1 {
411 cors_layer = cors_layer.allow_origin(origins[0].clone());
412 } else {
413 warn!(
415 "Multiple CORS origins configured, using permissive CORS. \
416 Consider using '*' for all origins."
417 );
418 cors_layer = cors_layer.allow_origin(Any);
419 }
420 }
421 } else {
422 cors_layer = cors_layer.allow_origin(Any);
424 }
425
426 if !config.allowed_methods.is_empty() {
428 let methods: Vec<Method> =
429 config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
430 if !methods.is_empty() {
431 cors_layer = cors_layer.allow_methods(methods);
432 }
433 } else {
434 cors_layer = cors_layer.allow_methods([
436 Method::GET,
437 Method::POST,
438 Method::PUT,
439 Method::DELETE,
440 Method::PATCH,
441 Method::OPTIONS,
442 ]);
443 }
444
445 if !config.allowed_headers.is_empty() {
447 let headers: Vec<_> = config
448 .allowed_headers
449 .iter()
450 .filter_map(|h| h.parse::<http::HeaderName>().ok())
451 .collect();
452 if !headers.is_empty() {
453 cors_layer = cors_layer.allow_headers(headers);
454 }
455 } else {
456 cors_layer =
458 cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
459 }
460
461 cors_layer = cors_layer.allow_credentials(true);
463
464 info!("CORS middleware enabled with configured settings");
465 app.layer(cors_layer)
466 } else {
467 debug!("No CORS config provided, using permissive CORS for development");
469 app.layer(CorsLayer::permissive())
470 }
471}
472
473#[allow(clippy::too_many_arguments)]
475pub async fn build_router_with_multi_tenant(
476 spec_path: Option<String>,
477 options: Option<ValidationOptions>,
478 failure_config: Option<FailureConfig>,
479 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
480 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
481 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
482 ai_generator: Option<
483 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
484 >,
485 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
486 mockai: Option<
487 std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
488 >,
489 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
490) -> Router {
491 use std::time::Instant;
492
493 let startup_start = Instant::now();
494
495 let mut app = Router::new();
497
498 let mut rate_limit_config = crate::middleware::RateLimitConfig {
501 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
502 .ok()
503 .and_then(|v| v.parse().ok())
504 .unwrap_or(1000),
505 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
506 .ok()
507 .and_then(|v| v.parse().ok())
508 .unwrap_or(2000),
509 per_ip: true,
510 per_endpoint: false,
511 };
512
513 let mut final_cors_config = cors_config;
515 let mut production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>> =
516 None;
517 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
519
520 if let Some(deploy_config) = &deceptive_deploy_config {
521 if deploy_config.enabled {
522 info!("Deceptive deploy mode enabled - applying production-like configuration");
523
524 if let Some(prod_cors) = &deploy_config.cors {
526 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
527 enabled: true,
528 allowed_origins: prod_cors.allowed_origins.clone(),
529 allowed_methods: prod_cors.allowed_methods.clone(),
530 allowed_headers: prod_cors.allowed_headers.clone(),
531 });
532 info!("Applied production-like CORS configuration");
533 }
534
535 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
537 rate_limit_config = crate::middleware::RateLimitConfig {
538 requests_per_minute: prod_rate_limit.requests_per_minute,
539 burst: prod_rate_limit.burst,
540 per_ip: prod_rate_limit.per_ip,
541 per_endpoint: false,
542 };
543 info!(
544 "Applied production-like rate limiting: {} req/min, burst: {}",
545 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
546 );
547 }
548
549 if !deploy_config.headers.is_empty() {
551 let headers_map: std::collections::HashMap<String, String> =
552 deploy_config.headers.clone();
553 production_headers = Some(std::sync::Arc::new(headers_map));
554 info!("Configured {} production headers", deploy_config.headers.len());
555 }
556
557 if let Some(prod_oauth) = &deploy_config.oauth {
559 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
560 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
561 oauth2: Some(oauth2_config),
562 ..Default::default()
563 });
564 info!("Applied production-like OAuth configuration for deceptive deploy");
565 }
566 }
567 }
568
569 let rate_limiter =
570 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
571
572 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
573
574 if let Some(headers) = production_headers.clone() {
576 state = state.with_production_headers(headers);
577 }
578
579 let spec_path_for_mgmt = spec_path.clone();
581
582 if let Some(spec_path) = spec_path {
584 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
585
586 let spec_load_start = Instant::now();
588 match OpenApiSpec::from_file(&spec_path).await {
589 Ok(openapi) => {
590 let spec_load_duration = spec_load_start.elapsed();
591 info!(
592 "Successfully loaded OpenAPI spec from {} (took {:?})",
593 spec_path, spec_load_duration
594 );
595
596 tracing::debug!("Creating OpenAPI route registry...");
598 let registry_start = Instant::now();
599 let registry = if let Some(opts) = options {
600 tracing::debug!("Using custom validation options");
601 OpenApiRouteRegistry::new_with_options(openapi, opts)
602 } else {
603 tracing::debug!("Using environment-based options");
604 OpenApiRouteRegistry::new_with_env(openapi)
605 };
606 let registry_duration = registry_start.elapsed();
607 info!(
608 "Created OpenAPI route registry with {} routes (took {:?})",
609 registry.routes().len(),
610 registry_duration
611 );
612
613 let extract_start = Instant::now();
615 let route_info: Vec<RouteInfo> = registry
616 .routes()
617 .iter()
618 .map(|route| RouteInfo {
619 method: route.method.clone(),
620 path: route.path.clone(),
621 operation_id: route.operation.operation_id.clone(),
622 summary: route.operation.summary.clone(),
623 description: route.operation.description.clone(),
624 parameters: route.parameters.clone(),
625 })
626 .collect();
627 state.routes = route_info;
628 let extract_duration = extract_start.elapsed();
629 debug!("Extracted route information (took {:?})", extract_duration);
630
631 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
633 tracing::debug!("Loading overrides from environment variable");
634 let overrides_start = Instant::now();
635 match mockforge_core::Overrides::load_from_globs(&[]).await {
636 Ok(overrides) => {
637 let overrides_duration = overrides_start.elapsed();
638 info!(
639 "Loaded {} override rules (took {:?})",
640 overrides.rules().len(),
641 overrides_duration
642 );
643 Some(overrides)
644 }
645 Err(e) => {
646 tracing::warn!("Failed to load overrides: {}", e);
647 None
648 }
649 }
650 } else {
651 None
652 };
653
654 let router_build_start = Instant::now();
656 let overrides_enabled = overrides.is_some();
657 let openapi_router = if let Some(mockai_instance) = &mockai {
658 tracing::debug!("Building router with MockAI support");
659 registry.build_router_with_mockai(Some(mockai_instance.clone()))
660 } else if let Some(ai_generator) = &ai_generator {
661 tracing::debug!("Building router with AI generator support");
662 registry.build_router_with_ai(Some(ai_generator.clone()))
663 } else if let Some(failure_config) = &failure_config {
664 tracing::debug!("Building router with failure injection and overrides");
665 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
666 registry.build_router_with_injectors_and_overrides(
667 LatencyInjector::default(),
668 Some(failure_injector),
669 overrides,
670 overrides_enabled,
671 )
672 } else {
673 tracing::debug!("Building router with overrides");
674 registry.build_router_with_injectors_and_overrides(
675 LatencyInjector::default(),
676 None,
677 overrides,
678 overrides_enabled,
679 )
680 };
681 let router_build_duration = router_build_start.elapsed();
682 debug!("Built OpenAPI router (took {:?})", router_build_duration);
683
684 tracing::debug!("Merging OpenAPI router with main router");
685 app = app.merge(openapi_router);
686 tracing::debug!("Router built successfully");
687 }
688 Err(e) => {
689 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
690 }
691 }
692 }
693
694 app = app.route(
696 "/health",
697 axum::routing::get(|| async {
698 use mockforge_core::server_utils::health::HealthStatus;
699 {
700 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
702 Ok(value) => axum::Json(value),
703 Err(e) => {
704 tracing::error!("Failed to serialize health status: {}", e);
706 axum::Json(serde_json::json!({
707 "status": "healthy",
708 "service": "mockforge-http",
709 "uptime_seconds": 0
710 }))
711 }
712 }
713 }
714 }),
715 )
716 .merge(sse::sse_router())
718 .merge(file_server::file_serving_router());
720
721 let state_for_routes = state.clone();
723
724 let routes_router = Router::new()
726 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
727 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
728 .with_state(state_for_routes);
729
730 app = app.merge(routes_router);
732
733 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
736 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
737
738 if std::path::Path::new(&coverage_html_path).exists() {
740 app = app.nest_service(
741 "/__mockforge/coverage.html",
742 tower_http::services::ServeFile::new(&coverage_html_path),
743 );
744 debug!("Serving coverage UI from: {}", coverage_html_path);
745 } else {
746 debug!(
747 "Coverage UI file not found at: {}. Skipping static file serving.",
748 coverage_html_path
749 );
750 }
751
752 let mut management_state = ManagementState::new(None, spec_path_for_mgmt, 3000); use std::sync::Arc;
757 let ws_state = WsManagementState::new();
758 let ws_broadcast = Arc::new(ws_state.tx.clone());
759 let management_state = management_state.with_ws_broadcast(ws_broadcast);
760
761 #[cfg(feature = "smtp")]
765 let management_state = {
766 if let Some(smtp_reg) = smtp_registry {
767 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
768 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
769 Err(e) => {
770 error!(
771 "Invalid SMTP registry type passed to HTTP management state: {:?}",
772 e.type_id()
773 );
774 management_state
775 }
776 }
777 } else {
778 management_state
779 }
780 };
781 #[cfg(not(feature = "smtp"))]
782 let management_state = management_state;
783 #[cfg(not(feature = "smtp"))]
784 let _ = smtp_registry;
785 app = app.nest("/__mockforge/api", management_router(management_state));
786
787 app = app.merge(verification_router());
789
790 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
792
793 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
795
796 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
799
800 app = app.layer(from_fn_with_state(state.clone(), crate::middleware::rate_limit_middleware));
802
803 if state.production_headers.is_some() {
805 app = app.layer(from_fn_with_state(
806 state.clone(),
807 crate::middleware::production_headers_middleware,
808 ));
809 }
810
811 if let Some(auth_config) = deceptive_deploy_auth_config {
813 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
814 use std::collections::HashMap;
815 use std::sync::Arc;
816 use tokio::sync::RwLock;
817
818 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
820 match create_oauth2_client(oauth2_config) {
821 Ok(client) => Some(client),
822 Err(e) => {
823 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
824 None
825 }
826 }
827 } else {
828 None
829 };
830
831 let auth_state = AuthState {
833 config: auth_config,
834 spec: None, oauth2_client,
836 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
837 };
838
839 app = app.layer(axum::middleware::from_fn_with_state(auth_state, auth_middleware));
841 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
842 }
843
844 app = apply_cors_middleware(app, final_cors_config);
846
847 if let Some(mt_config) = multi_tenant_config {
849 if mt_config.enabled {
850 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
851 use std::sync::Arc;
852
853 info!(
854 "Multi-tenant mode enabled with {} routing strategy",
855 match mt_config.routing_strategy {
856 mockforge_core::RoutingStrategy::Path => "path-based",
857 mockforge_core::RoutingStrategy::Port => "port-based",
858 mockforge_core::RoutingStrategy::Both => "hybrid",
859 }
860 );
861
862 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
864
865 let default_workspace =
867 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
868 if let Err(e) =
869 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
870 {
871 warn!("Failed to register default workspace: {}", e);
872 } else {
873 info!("Registered default workspace: '{}'", mt_config.default_workspace);
874 }
875
876 if mt_config.auto_discover {
878 if let Some(config_dir) = &mt_config.config_directory {
879 let config_path = Path::new(config_dir);
880 if config_path.exists() && config_path.is_dir() {
881 match fs::read_dir(config_path).await {
882 Ok(mut entries) => {
883 while let Ok(Some(entry)) = entries.next_entry().await {
884 let path = entry.path();
885 if path.extension() == Some(OsStr::new("yaml")) {
886 match fs::read_to_string(&path).await {
887 Ok(content) => {
888 match serde_yaml::from_str::<
889 mockforge_core::Workspace,
890 >(
891 &content
892 ) {
893 Ok(workspace) => {
894 if let Err(e) = registry.register_workspace(
895 workspace.id.clone(),
896 workspace,
897 ) {
898 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
899 } else {
900 info!("Auto-registered workspace from {:?}", path);
901 }
902 }
903 Err(e) => {
904 warn!("Failed to parse workspace from {:?}: {}", path, e);
905 }
906 }
907 }
908 Err(e) => {
909 warn!(
910 "Failed to read workspace file {:?}: {}",
911 path, e
912 );
913 }
914 }
915 }
916 }
917 }
918 Err(e) => {
919 warn!("Failed to read config directory {:?}: {}", config_path, e);
920 }
921 }
922 } else {
923 warn!(
924 "Config directory {:?} does not exist or is not a directory",
925 config_path
926 );
927 }
928 }
929 }
930
931 let registry = Arc::new(registry);
933
934 let _workspace_router = WorkspaceRouter::new(registry);
936
937 info!("Workspace routing middleware initialized for HTTP server");
940 }
941 }
942
943 let total_startup_duration = startup_start.elapsed();
944 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
945
946 app
947}
948
949pub async fn build_router_with_auth_and_latency(
951 _spec_path: Option<String>,
952 _options: Option<()>,
953 _auth_config: Option<mockforge_core::config::AuthConfig>,
954 _latency_injector: Option<LatencyInjector>,
955) -> Router {
956 build_router(None, None, None).await
958}
959
960pub async fn build_router_with_latency(
962 _spec_path: Option<String>,
963 _options: Option<ValidationOptions>,
964 _latency_injector: Option<LatencyInjector>,
965) -> Router {
966 build_router(None, None, None).await
968}
969
970pub async fn build_router_with_auth(
972 spec_path: Option<String>,
973 options: Option<ValidationOptions>,
974 auth_config: Option<mockforge_core::config::AuthConfig>,
975) -> Router {
976 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
977 use std::sync::Arc;
978
979 #[cfg(feature = "data-faker")]
981 {
982 register_core_faker_provider();
983 }
984
985 let spec = if let Some(spec_path) = &spec_path {
987 match mockforge_core::openapi::OpenApiSpec::from_file(&spec_path).await {
988 Ok(spec) => Some(Arc::new(spec)),
989 Err(e) => {
990 warn!("Failed to load OpenAPI spec for auth: {}", e);
991 None
992 }
993 }
994 } else {
995 None
996 };
997
998 let oauth2_client = if let Some(auth_config) = &auth_config {
1000 if let Some(oauth2_config) = &auth_config.oauth2 {
1001 match create_oauth2_client(oauth2_config) {
1002 Ok(client) => Some(client),
1003 Err(e) => {
1004 warn!("Failed to create OAuth2 client: {}", e);
1005 None
1006 }
1007 }
1008 } else {
1009 None
1010 }
1011 } else {
1012 None
1013 };
1014
1015 let auth_state = AuthState {
1016 config: auth_config.unwrap_or_default(),
1017 spec,
1018 oauth2_client,
1019 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1020 };
1021
1022 let mut app = Router::new().with_state(auth_state.clone());
1024
1025 if let Some(spec_path) = spec_path {
1027 match OpenApiSpec::from_file(&spec_path).await {
1028 Ok(openapi) => {
1029 info!("Loaded OpenAPI spec from {}", spec_path);
1030 let registry = if let Some(opts) = options {
1031 OpenApiRouteRegistry::new_with_options(openapi, opts)
1032 } else {
1033 OpenApiRouteRegistry::new_with_env(openapi)
1034 };
1035
1036 app = registry.build_router();
1037 }
1038 Err(e) => {
1039 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1040 }
1041 }
1042 }
1043
1044 app = app.route(
1046 "/health",
1047 axum::routing::get(|| async {
1048 use mockforge_core::server_utils::health::HealthStatus;
1049 {
1050 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1052 Ok(value) => axum::Json(value),
1053 Err(e) => {
1054 tracing::error!("Failed to serialize health status: {}", e);
1056 axum::Json(serde_json::json!({
1057 "status": "healthy",
1058 "service": "mockforge-http",
1059 "uptime_seconds": 0
1060 }))
1061 }
1062 }
1063 }
1064 }),
1065 )
1066 .merge(sse::sse_router())
1068 .merge(file_server::file_serving_router())
1070 .layer(axum::middleware::from_fn_with_state(auth_state.clone(), auth_middleware))
1072 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1074
1075 app
1076}
1077
1078pub async fn serve_router(
1080 port: u16,
1081 app: Router,
1082) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1083 serve_router_with_tls(port, app, None).await
1084}
1085
1086pub async fn serve_router_with_tls(
1088 port: u16,
1089 app: Router,
1090 tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1091) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1092 use std::net::SocketAddr;
1093
1094 let addr = mockforge_core::wildcard_socket_addr(port);
1095
1096 if let Some(ref tls) = tls_config {
1097 if tls.enabled {
1098 info!("HTTPS listening on {}", addr);
1099 return serve_with_tls(addr, app, tls).await;
1100 }
1101 }
1102
1103 info!("HTTP listening on {}", addr);
1104
1105 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1106 format!(
1107 "Failed to bind HTTP server to port {}: {}\n\
1108 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 {}",
1109 port, e, port, port
1110 )
1111 })?;
1112
1113 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
1114 Ok(())
1115}
1116
1117async fn serve_with_tls(
1123 addr: std::net::SocketAddr,
1124 _app: Router,
1125 tls_config: &mockforge_core::config::HttpTlsConfig,
1126) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1127 let _acceptor = tls::load_tls_acceptor(tls_config)?;
1129
1130 Err(format!(
1133 "TLS/HTTPS support is configured but requires a reverse proxy (nginx) for production use.\n\
1134 Certificate validation passed: {} and {}\n\
1135 For native TLS support, please use a reverse proxy or wait for axum-server integration.\n\
1136 You can configure nginx with TLS termination pointing to the HTTP server on port {}.",
1137 tls_config.cert_file,
1138 tls_config.key_file,
1139 addr.port()
1140 )
1141 .into())
1142}
1143
1144pub async fn start(
1146 port: u16,
1147 spec_path: Option<String>,
1148 options: Option<ValidationOptions>,
1149) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1150 start_with_latency(port, spec_path, options, None).await
1151}
1152
1153pub async fn start_with_auth_and_latency(
1155 port: u16,
1156 spec_path: Option<String>,
1157 options: Option<ValidationOptions>,
1158 auth_config: Option<mockforge_core::config::AuthConfig>,
1159 latency_profile: Option<LatencyProfile>,
1160) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1161 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1162 .await
1163}
1164
1165pub async fn start_with_auth_and_injectors(
1167 port: u16,
1168 spec_path: Option<String>,
1169 options: Option<ValidationOptions>,
1170 auth_config: Option<mockforge_core::config::AuthConfig>,
1171 _latency_profile: Option<LatencyProfile>,
1172 _failure_injector: Option<mockforge_core::FailureInjector>,
1173) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1174 let app = build_router_with_auth(spec_path, options, auth_config).await;
1176 serve_router(port, app).await
1177}
1178
1179pub async fn start_with_latency(
1181 port: u16,
1182 spec_path: Option<String>,
1183 options: Option<ValidationOptions>,
1184 latency_profile: Option<LatencyProfile>,
1185) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1186 let latency_injector =
1187 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1188
1189 let app = build_router_with_latency(spec_path, options, latency_injector).await;
1190 serve_router(port, app).await
1191}
1192
1193pub async fn build_router_with_chains(
1195 spec_path: Option<String>,
1196 options: Option<ValidationOptions>,
1197 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1198) -> Router {
1199 build_router_with_chains_and_multi_tenant(
1200 spec_path,
1201 options,
1202 circling_config,
1203 None,
1204 None,
1205 None,
1206 None,
1207 None,
1208 None,
1209 None,
1210 false,
1211 None, None, None, None, )
1216 .await
1217}
1218
1219#[allow(clippy::too_many_arguments)]
1221pub async fn build_router_with_chains_and_multi_tenant(
1222 spec_path: Option<String>,
1223 options: Option<ValidationOptions>,
1224 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1225 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
1226 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1227 cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1228 _ai_generator: Option<
1229 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
1230 >,
1231 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
1232 mqtt_broker: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
1233 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1234 traffic_shaping_enabled: bool,
1235 health_manager: Option<std::sync::Arc<health::HealthManager>>,
1236 _mockai: Option<
1237 std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
1238 >,
1239 deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1240 proxy_config: Option<mockforge_core::proxy::config::ProxyConfig>,
1241) -> Router {
1242 use crate::latency_profiles::LatencyProfiles;
1243 use crate::op_middleware::Shared;
1244 use mockforge_core::Overrides;
1245
1246 let _shared = Shared {
1247 profiles: LatencyProfiles::default(),
1248 overrides: Overrides::default(),
1249 failure_injector: None,
1250 traffic_shaper,
1251 overrides_enabled: false,
1252 traffic_shaping_enabled,
1253 };
1254
1255 let mut app = Router::new();
1257 let mut include_default_health = true;
1258
1259 if let Some(ref spec) = spec_path {
1261 match OpenApiSpec::from_file(&spec).await {
1262 Ok(openapi) => {
1263 info!("Loaded OpenAPI spec from {}", spec);
1264 let registry = if let Some(opts) = options {
1265 OpenApiRouteRegistry::new_with_options(openapi, opts)
1266 } else {
1267 OpenApiRouteRegistry::new_with_env(openapi)
1268 };
1269 if registry
1270 .routes()
1271 .iter()
1272 .any(|route| route.method == "GET" && route.path == "/health")
1273 {
1274 include_default_health = false;
1275 }
1276 let spec_router = if let Some(ref mockai_instance) = _mockai {
1278 tracing::debug!("Building router with MockAI support");
1279 registry.build_router_with_mockai(Some(mockai_instance.clone()))
1280 } else {
1281 registry.build_router()
1282 };
1283 app = app.merge(spec_router);
1284 }
1285 Err(e) => {
1286 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1287 }
1288 }
1289 }
1290
1291 if let Some(health) = health_manager {
1293 app = app.merge(health::health_router(health));
1295 info!(
1296 "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
1297 );
1298 } else if include_default_health {
1299 app = app.route(
1301 "/health",
1302 axum::routing::get(|| async {
1303 use mockforge_core::server_utils::health::HealthStatus;
1304 {
1305 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1307 Ok(value) => axum::Json(value),
1308 Err(e) => {
1309 tracing::error!("Failed to serialize health status: {}", e);
1311 axum::Json(serde_json::json!({
1312 "status": "healthy",
1313 "service": "mockforge-http",
1314 "uptime_seconds": 0
1315 }))
1316 }
1317 }
1318 }
1319 }),
1320 );
1321 }
1322
1323 app = app.merge(sse::sse_router());
1324 app = app.merge(file_server::file_serving_router());
1326
1327 let mut management_state = ManagementState::new(None, spec_path, 3000); use std::sync::Arc;
1332 let ws_state = WsManagementState::new();
1333 let ws_broadcast = Arc::new(ws_state.tx.clone());
1334 let management_state = management_state.with_ws_broadcast(ws_broadcast);
1335
1336 let management_state = if let Some(proxy_cfg) = proxy_config {
1338 use tokio::sync::RwLock;
1339 let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
1340 management_state.with_proxy_config(proxy_config_arc)
1341 } else {
1342 management_state
1343 };
1344
1345 #[cfg(feature = "smtp")]
1346 let management_state = {
1347 if let Some(smtp_reg) = smtp_registry {
1348 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
1349 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
1350 Err(e) => {
1351 error!(
1352 "Invalid SMTP registry type passed to HTTP management state: {:?}",
1353 e.type_id()
1354 );
1355 management_state
1356 }
1357 }
1358 } else {
1359 management_state
1360 }
1361 };
1362 #[cfg(not(feature = "smtp"))]
1363 let management_state = {
1364 let _ = smtp_registry;
1365 management_state
1366 };
1367 #[cfg(feature = "mqtt")]
1368 let management_state = {
1369 if let Some(broker) = mqtt_broker {
1370 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
1371 Ok(broker) => management_state.with_mqtt_broker(broker),
1372 Err(e) => {
1373 error!(
1374 "Invalid MQTT broker passed to HTTP management state: {:?}",
1375 e.type_id()
1376 );
1377 management_state
1378 }
1379 }
1380 } else {
1381 management_state
1382 }
1383 };
1384 #[cfg(not(feature = "mqtt"))]
1385 let management_state = {
1386 let _ = mqtt_broker;
1387 management_state
1388 };
1389 app = app.nest("/__mockforge/api", management_router(management_state));
1390
1391 app = app.merge(verification_router());
1393
1394 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1396
1397 if let Some(mt_config) = multi_tenant_config {
1399 if mt_config.enabled {
1400 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1401 use std::sync::Arc;
1402
1403 info!(
1404 "Multi-tenant mode enabled with {} routing strategy",
1405 match mt_config.routing_strategy {
1406 mockforge_core::RoutingStrategy::Path => "path-based",
1407 mockforge_core::RoutingStrategy::Port => "port-based",
1408 mockforge_core::RoutingStrategy::Both => "hybrid",
1409 }
1410 );
1411
1412 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1414
1415 let default_workspace =
1417 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1418 if let Err(e) =
1419 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1420 {
1421 warn!("Failed to register default workspace: {}", e);
1422 } else {
1423 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1424 }
1425
1426 let registry = Arc::new(registry);
1428
1429 let _workspace_router = WorkspaceRouter::new(registry);
1431 info!("Workspace routing middleware initialized for HTTP server");
1432 }
1433 }
1434
1435 let mut final_cors_config = cors_config;
1437 let mut production_headers: Option<std::sync::Arc<std::collections::HashMap<String, String>>> =
1438 None;
1439 let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
1441 let mut rate_limit_config = crate::middleware::RateLimitConfig {
1442 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
1443 .ok()
1444 .and_then(|v| v.parse().ok())
1445 .unwrap_or(1000),
1446 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
1447 .ok()
1448 .and_then(|v| v.parse().ok())
1449 .unwrap_or(2000),
1450 per_ip: true,
1451 per_endpoint: false,
1452 };
1453
1454 if let Some(deploy_config) = &deceptive_deploy_config {
1455 if deploy_config.enabled {
1456 info!("Deceptive deploy mode enabled - applying production-like configuration");
1457
1458 if let Some(prod_cors) = &deploy_config.cors {
1460 final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
1461 enabled: true,
1462 allowed_origins: prod_cors.allowed_origins.clone(),
1463 allowed_methods: prod_cors.allowed_methods.clone(),
1464 allowed_headers: prod_cors.allowed_headers.clone(),
1465 });
1466 info!("Applied production-like CORS configuration");
1467 }
1468
1469 if let Some(prod_rate_limit) = &deploy_config.rate_limit {
1471 rate_limit_config = crate::middleware::RateLimitConfig {
1472 requests_per_minute: prod_rate_limit.requests_per_minute,
1473 burst: prod_rate_limit.burst,
1474 per_ip: prod_rate_limit.per_ip,
1475 per_endpoint: false,
1476 };
1477 info!(
1478 "Applied production-like rate limiting: {} req/min, burst: {}",
1479 prod_rate_limit.requests_per_minute, prod_rate_limit.burst
1480 );
1481 }
1482
1483 if !deploy_config.headers.is_empty() {
1485 let headers_map: std::collections::HashMap<String, String> =
1486 deploy_config.headers.clone();
1487 production_headers = Some(std::sync::Arc::new(headers_map));
1488 info!("Configured {} production headers", deploy_config.headers.len());
1489 }
1490
1491 if let Some(prod_oauth) = &deploy_config.oauth {
1493 let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
1494 deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
1495 oauth2: Some(oauth2_config),
1496 ..Default::default()
1497 });
1498 info!("Applied production-like OAuth configuration for deceptive deploy");
1499 }
1500 }
1501 }
1502
1503 let rate_limiter =
1505 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
1506
1507 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
1508
1509 if let Some(headers) = production_headers.clone() {
1511 state = state.with_production_headers(headers);
1512 }
1513
1514 app = app.layer(from_fn_with_state(state.clone(), crate::middleware::rate_limit_middleware));
1516
1517 if state.production_headers.is_some() {
1519 app = app.layer(from_fn_with_state(
1520 state.clone(),
1521 crate::middleware::production_headers_middleware,
1522 ));
1523 }
1524
1525 if let Some(auth_config) = deceptive_deploy_auth_config {
1527 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1528 use std::collections::HashMap;
1529 use std::sync::Arc;
1530 use tokio::sync::RwLock;
1531
1532 let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1534 match create_oauth2_client(oauth2_config) {
1535 Ok(client) => Some(client),
1536 Err(e) => {
1537 warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1538 None
1539 }
1540 }
1541 } else {
1542 None
1543 };
1544
1545 let auth_state = AuthState {
1547 config: auth_config,
1548 spec: None, oauth2_client,
1550 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1551 };
1552
1553 app = app.layer(axum::middleware::from_fn_with_state(auth_state, auth_middleware));
1555 info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1556 }
1557
1558 app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1561
1562 app = apply_cors_middleware(app, final_cors_config);
1564
1565 app
1566}
1567
1568#[test]
1572fn test_route_info_clone() {
1573 let route = RouteInfo {
1574 method: "POST".to_string(),
1575 path: "/users".to_string(),
1576 operation_id: Some("createUser".to_string()),
1577 summary: None,
1578 description: None,
1579 parameters: vec![],
1580 };
1581
1582 let cloned = route.clone();
1583 assert_eq!(route.method, cloned.method);
1584 assert_eq!(route.path, cloned.path);
1585 assert_eq!(route.operation_id, cloned.operation_id);
1586}
1587
1588#[test]
1589fn test_http_server_state_new() {
1590 let state = HttpServerState::new();
1591 assert_eq!(state.routes.len(), 0);
1592}
1593
1594#[test]
1595fn test_http_server_state_with_routes() {
1596 let routes = vec![
1597 RouteInfo {
1598 method: "GET".to_string(),
1599 path: "/users".to_string(),
1600 operation_id: Some("getUsers".to_string()),
1601 summary: None,
1602 description: None,
1603 parameters: vec![],
1604 },
1605 RouteInfo {
1606 method: "POST".to_string(),
1607 path: "/users".to_string(),
1608 operation_id: Some("createUser".to_string()),
1609 summary: None,
1610 description: None,
1611 parameters: vec![],
1612 },
1613 ];
1614
1615 let state = HttpServerState::with_routes(routes.clone());
1616 assert_eq!(state.routes.len(), 2);
1617 assert_eq!(state.routes[0].method, "GET");
1618 assert_eq!(state.routes[1].method, "POST");
1619}
1620
1621#[test]
1622fn test_http_server_state_clone() {
1623 let routes = vec![RouteInfo {
1624 method: "GET".to_string(),
1625 path: "/test".to_string(),
1626 operation_id: None,
1627 summary: None,
1628 description: None,
1629 parameters: vec![],
1630 }];
1631
1632 let state = HttpServerState::with_routes(routes);
1633 let cloned = state.clone();
1634
1635 assert_eq!(state.routes.len(), cloned.routes.len());
1636 assert_eq!(state.routes[0].method, cloned.routes[0].method);
1637}
1638
1639#[tokio::test]
1640async fn test_build_router_without_openapi() {
1641 let _router = build_router(None, None, None).await;
1642 }
1644
1645#[tokio::test]
1646async fn test_build_router_with_nonexistent_spec() {
1647 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
1648 }
1650
1651#[tokio::test]
1652async fn test_build_router_with_auth_and_latency() {
1653 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
1654 }
1656
1657#[tokio::test]
1658async fn test_build_router_with_latency() {
1659 let _router = build_router_with_latency(None, None, None).await;
1660 }
1662
1663#[tokio::test]
1664async fn test_build_router_with_auth() {
1665 let _router = build_router_with_auth(None, None, None).await;
1666 }
1668
1669#[tokio::test]
1670async fn test_build_router_with_chains() {
1671 let _router = build_router_with_chains(None, None, None).await;
1672 }
1674
1675#[test]
1676fn test_route_info_with_all_fields() {
1677 let route = RouteInfo {
1678 method: "PUT".to_string(),
1679 path: "/users/{id}".to_string(),
1680 operation_id: Some("updateUser".to_string()),
1681 summary: Some("Update user".to_string()),
1682 description: Some("Updates an existing user".to_string()),
1683 parameters: vec!["id".to_string(), "body".to_string()],
1684 };
1685
1686 assert!(route.operation_id.is_some());
1687 assert!(route.summary.is_some());
1688 assert!(route.description.is_some());
1689 assert_eq!(route.parameters.len(), 2);
1690}
1691
1692#[test]
1693fn test_route_info_with_minimal_fields() {
1694 let route = RouteInfo {
1695 method: "DELETE".to_string(),
1696 path: "/users/{id}".to_string(),
1697 operation_id: None,
1698 summary: None,
1699 description: None,
1700 parameters: vec![],
1701 };
1702
1703 assert!(route.operation_id.is_none());
1704 assert!(route.summary.is_none());
1705 assert!(route.description.is_none());
1706 assert_eq!(route.parameters.len(), 0);
1707}
1708
1709#[test]
1710fn test_http_server_state_empty_routes() {
1711 let state = HttpServerState::with_routes(vec![]);
1712 assert_eq!(state.routes.len(), 0);
1713}
1714
1715#[test]
1716fn test_http_server_state_multiple_routes() {
1717 let routes = vec![
1718 RouteInfo {
1719 method: "GET".to_string(),
1720 path: "/users".to_string(),
1721 operation_id: Some("listUsers".to_string()),
1722 summary: Some("List all users".to_string()),
1723 description: None,
1724 parameters: vec![],
1725 },
1726 RouteInfo {
1727 method: "GET".to_string(),
1728 path: "/users/{id}".to_string(),
1729 operation_id: Some("getUser".to_string()),
1730 summary: Some("Get a user".to_string()),
1731 description: None,
1732 parameters: vec!["id".to_string()],
1733 },
1734 RouteInfo {
1735 method: "POST".to_string(),
1736 path: "/users".to_string(),
1737 operation_id: Some("createUser".to_string()),
1738 summary: Some("Create a user".to_string()),
1739 description: None,
1740 parameters: vec!["body".to_string()],
1741 },
1742 ];
1743
1744 let state = HttpServerState::with_routes(routes);
1745 assert_eq!(state.routes.len(), 3);
1746
1747 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
1749 assert!(methods.contains(&"GET"));
1750 assert!(methods.contains(&"POST"));
1751}
1752
1753#[test]
1754fn test_http_server_state_with_rate_limiter() {
1755 use std::sync::Arc;
1756
1757 let config = crate::middleware::RateLimitConfig::default();
1758 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
1759
1760 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
1761
1762 assert!(state.rate_limiter.is_some());
1763 assert_eq!(state.routes.len(), 0);
1764}
1765
1766#[tokio::test]
1767async fn test_build_router_includes_rate_limiter() {
1768 let _router = build_router(None, None, None).await;
1769 }