1pub mod ai_handler;
166pub mod auth;
167pub mod chain_handlers;
168pub mod coverage;
169pub mod http_tracing_middleware;
170pub mod latency_profiles;
172pub mod management;
174pub mod management_ws;
176pub mod metrics_middleware;
177pub mod middleware;
178pub mod op_middleware;
179pub mod quick_mock;
181pub mod rag_ai_generator;
183pub mod replay_listing;
185pub mod request_logging;
186pub mod spec_import;
188pub mod sse;
190pub mod token_response;
192pub mod ui_builder;
194
195pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
197
198pub use management::{
200 management_router, management_router_with_ui_builder, ManagementState, MockConfig,
201 ServerConfig, ServerStats,
202};
203
204pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
206
207pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
209
210pub use metrics_middleware::collect_http_metrics;
212
213pub use http_tracing_middleware::http_tracing_middleware;
215
216pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
218
219use axum::middleware::from_fn_with_state;
220use axum::{extract::State, response::Json, Router};
221use mockforge_core::failure_injection::{FailureConfig, FailureInjector};
222use mockforge_core::latency::LatencyInjector;
223use mockforge_core::openapi::OpenApiSpec;
224use mockforge_core::openapi_routes::OpenApiRouteRegistry;
225use mockforge_core::openapi_routes::ValidationOptions;
226
227use mockforge_core::LatencyProfile;
228#[cfg(feature = "data-faker")]
229use mockforge_data::provider::register_core_faker_provider;
230use std::collections::HashMap;
231use std::ffi::OsStr;
232use std::path::Path;
233use tokio::fs;
234use tokio::sync::RwLock;
235use tracing::*;
236
237#[derive(Clone)]
239pub struct RouteInfo {
240 pub method: String,
242 pub path: String,
244 pub operation_id: Option<String>,
246 pub summary: Option<String>,
248 pub description: Option<String>,
250 pub parameters: Vec<String>,
252}
253
254#[derive(Clone)]
256pub struct HttpServerState {
257 pub routes: Vec<RouteInfo>,
259 pub rate_limiter: Option<std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>>,
261}
262
263impl Default for HttpServerState {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl HttpServerState {
270 pub fn new() -> Self {
272 Self {
273 routes: Vec::new(),
274 rate_limiter: None,
275 }
276 }
277
278 pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
280 Self {
281 routes,
282 rate_limiter: None,
283 }
284 }
285
286 pub fn with_rate_limiter(
288 mut self,
289 rate_limiter: std::sync::Arc<crate::middleware::rate_limit::GlobalRateLimiter>,
290 ) -> Self {
291 self.rate_limiter = Some(rate_limiter);
292 self
293 }
294}
295
296async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
298 let route_info: Vec<serde_json::Value> = state
299 .routes
300 .iter()
301 .map(|route| {
302 serde_json::json!({
303 "method": route.method,
304 "path": route.path,
305 "operation_id": route.operation_id,
306 "summary": route.summary,
307 "description": route.description,
308 "parameters": route.parameters
309 })
310 })
311 .collect();
312
313 Json(serde_json::json!({
314 "routes": route_info,
315 "total": state.routes.len()
316 }))
317}
318
319pub async fn build_router(
321 spec_path: Option<String>,
322 options: Option<ValidationOptions>,
323 failure_config: Option<FailureConfig>,
324) -> Router {
325 build_router_with_multi_tenant(spec_path, options, failure_config, None, None, None, None, None)
326 .await
327}
328
329#[allow(clippy::too_many_arguments)]
331pub async fn build_router_with_multi_tenant(
332 spec_path: Option<String>,
333 options: Option<ValidationOptions>,
334 failure_config: Option<FailureConfig>,
335 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
336 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
337 _cors_config: Option<mockforge_core::config::HttpCorsConfig>,
338 ai_generator: Option<
339 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
340 >,
341 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
342) -> Router {
343 use std::time::Instant;
344
345 let startup_start = Instant::now();
346
347 let mut app = Router::new();
349
350 let rate_limit_config = crate::middleware::RateLimitConfig {
353 requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
354 .ok()
355 .and_then(|v| v.parse().ok())
356 .unwrap_or(1000),
357 burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
358 .ok()
359 .and_then(|v| v.parse().ok())
360 .unwrap_or(2000),
361 per_ip: true,
362 per_endpoint: false,
363 };
364 let rate_limiter =
365 std::sync::Arc::new(crate::middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
366
367 let mut state = HttpServerState::new().with_rate_limiter(rate_limiter.clone());
368
369 let spec_path_for_mgmt = spec_path.clone();
371
372 if let Some(spec_path) = spec_path {
374 tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
375
376 let spec_load_start = Instant::now();
378 match OpenApiSpec::from_file(&spec_path).await {
379 Ok(openapi) => {
380 let spec_load_duration = spec_load_start.elapsed();
381 info!(
382 "Successfully loaded OpenAPI spec from {} (took {:?})",
383 spec_path, spec_load_duration
384 );
385
386 tracing::debug!("Creating OpenAPI route registry...");
388 let registry_start = Instant::now();
389 let registry = if let Some(opts) = options {
390 tracing::debug!("Using custom validation options");
391 OpenApiRouteRegistry::new_with_options(openapi, opts)
392 } else {
393 tracing::debug!("Using environment-based options");
394 OpenApiRouteRegistry::new_with_env(openapi)
395 };
396 let registry_duration = registry_start.elapsed();
397 info!(
398 "Created OpenAPI route registry with {} routes (took {:?})",
399 registry.routes().len(),
400 registry_duration
401 );
402
403 let extract_start = Instant::now();
405 let route_info: Vec<RouteInfo> = registry
406 .routes()
407 .iter()
408 .map(|route| RouteInfo {
409 method: route.method.clone(),
410 path: route.path.clone(),
411 operation_id: route.operation.operation_id.clone(),
412 summary: route.operation.summary.clone(),
413 description: route.operation.description.clone(),
414 parameters: route.parameters.clone(),
415 })
416 .collect();
417 state.routes = route_info;
418 let extract_duration = extract_start.elapsed();
419 debug!("Extracted route information (took {:?})", extract_duration);
420
421 let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
423 tracing::debug!("Loading overrides from environment variable");
424 let overrides_start = Instant::now();
425 match mockforge_core::Overrides::load_from_globs(&[]).await {
426 Ok(overrides) => {
427 let overrides_duration = overrides_start.elapsed();
428 info!(
429 "Loaded {} override rules (took {:?})",
430 overrides.rules().len(),
431 overrides_duration
432 );
433 Some(overrides)
434 }
435 Err(e) => {
436 tracing::warn!("Failed to load overrides: {}", e);
437 None
438 }
439 }
440 } else {
441 None
442 };
443
444 let router_build_start = Instant::now();
446 let overrides_enabled = overrides.is_some();
447 let openapi_router = if let Some(ai_generator) = &ai_generator {
448 tracing::debug!("Building router with AI generator support");
449 registry.build_router_with_ai(Some(ai_generator.clone()))
450 } else if let Some(failure_config) = &failure_config {
451 tracing::debug!("Building router with failure injection and overrides");
452 let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
453 registry.build_router_with_injectors_and_overrides(
454 LatencyInjector::default(),
455 Some(failure_injector),
456 overrides,
457 overrides_enabled,
458 )
459 } else {
460 tracing::debug!("Building router with overrides");
461 registry.build_router_with_injectors_and_overrides(
462 LatencyInjector::default(),
463 None,
464 overrides,
465 overrides_enabled,
466 )
467 };
468 let router_build_duration = router_build_start.elapsed();
469 debug!("Built OpenAPI router (took {:?})", router_build_duration);
470
471 tracing::debug!("Merging OpenAPI router with main router");
472 app = app.merge(openapi_router);
473 tracing::debug!("Router built successfully");
474 }
475 Err(e) => {
476 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
477 }
478 }
479 }
480
481 app = app.route(
483 "/health",
484 axum::routing::get(|| async {
485 use mockforge_core::server_utils::health::HealthStatus;
486 {
487 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
489 Ok(value) => axum::Json(value),
490 Err(e) => {
491 tracing::error!("Failed to serialize health status: {}", e);
493 axum::Json(serde_json::json!({
494 "status": "healthy",
495 "service": "mockforge-http",
496 "uptime_seconds": 0
497 }))
498 }
499 }
500 }
501 }),
502 )
503 .merge(sse::sse_router());
505
506 let state_for_routes = state.clone();
508
509 let routes_router = Router::new()
511 .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
512 .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
513 .with_state(state_for_routes);
514
515 app = app.merge(routes_router);
517
518 let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
521 .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
522
523 if std::path::Path::new(&coverage_html_path).exists() {
525 app = app.nest_service(
526 "/__mockforge/coverage.html",
527 tower_http::services::ServeFile::new(&coverage_html_path),
528 );
529 debug!("Serving coverage UI from: {}", coverage_html_path);
530 } else {
531 debug!(
532 "Coverage UI file not found at: {}. Skipping static file serving.",
533 coverage_html_path
534 );
535 }
536
537 let management_state = ManagementState::new(None, spec_path_for_mgmt, 3000); #[cfg(feature = "smtp")]
540 let management_state = {
541 if let Some(smtp_reg) = smtp_registry {
542 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
543 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
544 Err(e) => {
545 error!(
546 "Invalid SMTP registry type passed to HTTP management state: {:?}",
547 e.type_id()
548 );
549 management_state
550 }
551 }
552 } else {
553 management_state
554 }
555 };
556 #[cfg(not(feature = "smtp"))]
557 let management_state = management_state;
558 #[cfg(not(feature = "smtp"))]
559 let _ = smtp_registry;
560 app = app.nest("/__mockforge/api", management_router(management_state));
561
562 let ws_state = WsManagementState::new();
564 app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
565
566 app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
568
569 app = app.layer(from_fn_with_state(state, crate::middleware::rate_limit_middleware));
571
572 if let Some(mt_config) = multi_tenant_config {
574 if mt_config.enabled {
575 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
576 use std::sync::Arc;
577
578 info!(
579 "Multi-tenant mode enabled with {} routing strategy",
580 match mt_config.routing_strategy {
581 mockforge_core::RoutingStrategy::Path => "path-based",
582 mockforge_core::RoutingStrategy::Port => "port-based",
583 mockforge_core::RoutingStrategy::Both => "hybrid",
584 }
585 );
586
587 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
589
590 let default_workspace =
592 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
593 if let Err(e) =
594 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
595 {
596 warn!("Failed to register default workspace: {}", e);
597 } else {
598 info!("Registered default workspace: '{}'", mt_config.default_workspace);
599 }
600
601 if mt_config.auto_discover {
603 if let Some(config_dir) = &mt_config.config_directory {
604 let config_path = Path::new(config_dir);
605 if config_path.exists() && config_path.is_dir() {
606 match fs::read_dir(config_path).await {
607 Ok(mut entries) => {
608 while let Ok(Some(entry)) = entries.next_entry().await {
609 let path = entry.path();
610 if path.extension() == Some(OsStr::new("yaml")) {
611 match fs::read_to_string(&path).await {
612 Ok(content) => {
613 match serde_yaml::from_str::<
614 mockforge_core::Workspace,
615 >(
616 &content
617 ) {
618 Ok(workspace) => {
619 if let Err(e) = registry.register_workspace(
620 workspace.id.clone(),
621 workspace,
622 ) {
623 warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
624 } else {
625 info!("Auto-registered workspace from {:?}", path);
626 }
627 }
628 Err(e) => {
629 warn!("Failed to parse workspace from {:?}: {}", path, e);
630 }
631 }
632 }
633 Err(e) => {
634 warn!(
635 "Failed to read workspace file {:?}: {}",
636 path, e
637 );
638 }
639 }
640 }
641 }
642 }
643 Err(e) => {
644 warn!("Failed to read config directory {:?}: {}", config_path, e);
645 }
646 }
647 } else {
648 warn!(
649 "Config directory {:?} does not exist or is not a directory",
650 config_path
651 );
652 }
653 }
654 }
655
656 let registry = Arc::new(registry);
658
659 let _workspace_router = WorkspaceRouter::new(registry);
661
662 info!("Workspace routing middleware initialized for HTTP server");
665 }
666 }
667
668 let total_startup_duration = startup_start.elapsed();
669 info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
670
671 app
672}
673
674pub async fn build_router_with_auth_and_latency(
676 _spec_path: Option<String>,
677 _options: Option<()>,
678 _auth_config: Option<mockforge_core::config::AuthConfig>,
679 _latency_injector: Option<LatencyInjector>,
680) -> Router {
681 build_router(None, None, None).await
683}
684
685pub async fn build_router_with_latency(
687 _spec_path: Option<String>,
688 _options: Option<ValidationOptions>,
689 _latency_injector: Option<LatencyInjector>,
690) -> Router {
691 build_router(None, None, None).await
693}
694
695pub async fn build_router_with_auth(
697 spec_path: Option<String>,
698 options: Option<ValidationOptions>,
699 auth_config: Option<mockforge_core::config::AuthConfig>,
700) -> Router {
701 use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
702 use std::sync::Arc;
703
704 #[cfg(feature = "data-faker")]
706 {
707 register_core_faker_provider();
708 }
709
710 let spec = if let Some(spec_path) = &spec_path {
712 match mockforge_core::openapi::OpenApiSpec::from_file(&spec_path).await {
713 Ok(spec) => Some(Arc::new(spec)),
714 Err(e) => {
715 warn!("Failed to load OpenAPI spec for auth: {}", e);
716 None
717 }
718 }
719 } else {
720 None
721 };
722
723 let oauth2_client = if let Some(auth_config) = &auth_config {
725 if let Some(oauth2_config) = &auth_config.oauth2 {
726 match create_oauth2_client(oauth2_config) {
727 Ok(client) => Some(client),
728 Err(e) => {
729 warn!("Failed to create OAuth2 client: {}", e);
730 None
731 }
732 }
733 } else {
734 None
735 }
736 } else {
737 None
738 };
739
740 let auth_state = AuthState {
741 config: auth_config.unwrap_or_default(),
742 spec,
743 oauth2_client,
744 introspection_cache: Arc::new(RwLock::new(HashMap::new())),
745 };
746
747 let mut app = Router::new().with_state(auth_state.clone());
749
750 if let Some(spec_path) = spec_path {
752 match OpenApiSpec::from_file(&spec_path).await {
753 Ok(openapi) => {
754 info!("Loaded OpenAPI spec from {}", spec_path);
755 let registry = if let Some(opts) = options {
756 OpenApiRouteRegistry::new_with_options(openapi, opts)
757 } else {
758 OpenApiRouteRegistry::new_with_env(openapi)
759 };
760
761 app = registry.build_router();
762 }
763 Err(e) => {
764 warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
765 }
766 }
767 }
768
769 app = app.route(
771 "/health",
772 axum::routing::get(|| async {
773 use mockforge_core::server_utils::health::HealthStatus;
774 {
775 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
777 Ok(value) => axum::Json(value),
778 Err(e) => {
779 tracing::error!("Failed to serialize health status: {}", e);
781 axum::Json(serde_json::json!({
782 "status": "healthy",
783 "service": "mockforge-http",
784 "uptime_seconds": 0
785 }))
786 }
787 }
788 }
789 }),
790 )
791 .merge(sse::sse_router())
793 .layer(axum::middleware::from_fn_with_state(auth_state.clone(), auth_middleware))
795 .layer(axum::middleware::from_fn(request_logging::log_http_requests));
797
798 app
799}
800
801pub async fn serve_router(
803 port: u16,
804 app: Router,
805) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
806 use std::net::SocketAddr;
807
808 let addr = mockforge_core::wildcard_socket_addr(port);
809 info!("HTTP listening on {}", addr);
810
811 let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
812 format!(
813 "Failed to bind HTTP server to port {}: {}\n\
814 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 {}",
815 port, e, port, port
816 )
817 })?;
818
819 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
820 Ok(())
821}
822
823pub async fn start(
825 port: u16,
826 spec_path: Option<String>,
827 options: Option<ValidationOptions>,
828) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
829 start_with_latency(port, spec_path, options, None).await
830}
831
832pub async fn start_with_auth_and_latency(
834 port: u16,
835 spec_path: Option<String>,
836 options: Option<ValidationOptions>,
837 auth_config: Option<mockforge_core::config::AuthConfig>,
838 latency_profile: Option<LatencyProfile>,
839) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
840 start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
841 .await
842}
843
844pub async fn start_with_auth_and_injectors(
846 port: u16,
847 spec_path: Option<String>,
848 options: Option<ValidationOptions>,
849 auth_config: Option<mockforge_core::config::AuthConfig>,
850 _latency_profile: Option<LatencyProfile>,
851 _failure_injector: Option<mockforge_core::FailureInjector>,
852) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
853 let app = build_router_with_auth(spec_path, options, auth_config).await;
855 serve_router(port, app).await
856}
857
858pub async fn start_with_latency(
860 port: u16,
861 spec_path: Option<String>,
862 options: Option<ValidationOptions>,
863 latency_profile: Option<LatencyProfile>,
864) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
865 let latency_injector =
866 latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
867
868 let app = build_router_with_latency(spec_path, options, latency_injector).await;
869 serve_router(port, app).await
870}
871
872pub async fn build_router_with_chains(
874 spec_path: Option<String>,
875 options: Option<ValidationOptions>,
876 circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
877) -> Router {
878 build_router_with_chains_and_multi_tenant(
879 spec_path,
880 options,
881 circling_config,
882 None,
883 None,
884 None,
885 None,
886 None,
887 None,
888 None,
889 false,
890 )
891 .await
892}
893
894#[allow(clippy::too_many_arguments)]
896pub async fn build_router_with_chains_and_multi_tenant(
897 spec_path: Option<String>,
898 options: Option<ValidationOptions>,
899 _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
900 multi_tenant_config: Option<mockforge_core::MultiTenantConfig>,
901 _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
902 _cors_config: Option<mockforge_core::config::HttpCorsConfig>,
903 _ai_generator: Option<
904 std::sync::Arc<dyn mockforge_core::openapi::response::AiGenerator + Send + Sync>,
905 >,
906 smtp_registry: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
907 mqtt_broker: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
908 traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
909 traffic_shaping_enabled: bool,
910) -> Router {
911 use crate::latency_profiles::LatencyProfiles;
912 use crate::op_middleware::Shared;
913 use mockforge_core::Overrides;
914
915 let _shared = Shared {
916 profiles: LatencyProfiles::default(),
917 overrides: Overrides::default(),
918 failure_injector: None,
919 traffic_shaper,
920 overrides_enabled: false,
921 traffic_shaping_enabled,
922 };
923
924 let mut app = Router::new();
926 let mut include_default_health = true;
927
928 if let Some(ref spec) = spec_path {
930 match OpenApiSpec::from_file(&spec).await {
931 Ok(openapi) => {
932 info!("Loaded OpenAPI spec from {}", spec);
933 let registry = if let Some(opts) = options {
934 OpenApiRouteRegistry::new_with_options(openapi, opts)
935 } else {
936 OpenApiRouteRegistry::new_with_env(openapi)
937 };
938 if registry
939 .routes()
940 .iter()
941 .any(|route| route.method == "GET" && route.path == "/health")
942 {
943 include_default_health = false;
944 }
945 let spec_router = registry.build_router();
946 app = app.merge(spec_router);
947 }
948 Err(e) => {
949 warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
950 }
951 }
952 }
953
954 if include_default_health {
955 app = app.route(
956 "/health",
957 axum::routing::get(|| async {
958 use mockforge_core::server_utils::health::HealthStatus;
959 {
960 match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
962 Ok(value) => axum::Json(value),
963 Err(e) => {
964 tracing::error!("Failed to serialize health status: {}", e);
966 axum::Json(serde_json::json!({
967 "status": "healthy",
968 "service": "mockforge-http",
969 "uptime_seconds": 0
970 }))
971 }
972 }
973 }
974 }),
975 );
976 }
977
978 app = app.merge(sse::sse_router());
979
980 let management_state = ManagementState::new(None, spec_path, 3000); #[cfg(feature = "smtp")]
983 let management_state = {
984 if let Some(smtp_reg) = smtp_registry {
985 match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
986 Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
987 Err(e) => {
988 error!(
989 "Invalid SMTP registry type passed to HTTP management state: {:?}",
990 e.type_id()
991 );
992 management_state
993 }
994 }
995 } else {
996 management_state
997 }
998 };
999 #[cfg(not(feature = "smtp"))]
1000 let management_state = {
1001 let _ = smtp_registry;
1002 management_state
1003 };
1004 #[cfg(feature = "mqtt")]
1005 let management_state = {
1006 if let Some(broker) = mqtt_broker {
1007 match broker.downcast::<mockforge_mqtt::MqttBroker>() {
1008 Ok(broker) => management_state.with_mqtt_broker(broker),
1009 Err(e) => {
1010 error!(
1011 "Invalid MQTT broker passed to HTTP management state: {:?}",
1012 e.type_id()
1013 );
1014 management_state
1015 }
1016 }
1017 } else {
1018 management_state
1019 }
1020 };
1021 #[cfg(not(feature = "mqtt"))]
1022 let management_state = {
1023 let _ = mqtt_broker;
1024 management_state
1025 };
1026 app = app.nest("/__mockforge/api", management_router(management_state));
1027
1028 if let Some(mt_config) = multi_tenant_config {
1030 if mt_config.enabled {
1031 use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1032 use std::sync::Arc;
1033
1034 info!(
1035 "Multi-tenant mode enabled with {} routing strategy",
1036 match mt_config.routing_strategy {
1037 mockforge_core::RoutingStrategy::Path => "path-based",
1038 mockforge_core::RoutingStrategy::Port => "port-based",
1039 mockforge_core::RoutingStrategy::Both => "hybrid",
1040 }
1041 );
1042
1043 let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1045
1046 let default_workspace =
1048 mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1049 if let Err(e) =
1050 registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1051 {
1052 warn!("Failed to register default workspace: {}", e);
1053 } else {
1054 info!("Registered default workspace: '{}'", mt_config.default_workspace);
1055 }
1056
1057 let registry = Arc::new(registry);
1059
1060 let _workspace_router = WorkspaceRouter::new(registry);
1062 info!("Workspace routing middleware initialized for HTTP server");
1063 }
1064 }
1065
1066 app
1067}
1068
1069#[test]
1073fn test_route_info_clone() {
1074 let route = RouteInfo {
1075 method: "POST".to_string(),
1076 path: "/users".to_string(),
1077 operation_id: Some("createUser".to_string()),
1078 summary: None,
1079 description: None,
1080 parameters: vec![],
1081 };
1082
1083 let cloned = route.clone();
1084 assert_eq!(route.method, cloned.method);
1085 assert_eq!(route.path, cloned.path);
1086 assert_eq!(route.operation_id, cloned.operation_id);
1087}
1088
1089#[test]
1090fn test_http_server_state_new() {
1091 let state = HttpServerState::new();
1092 assert_eq!(state.routes.len(), 0);
1093}
1094
1095#[test]
1096fn test_http_server_state_with_routes() {
1097 let routes = vec![
1098 RouteInfo {
1099 method: "GET".to_string(),
1100 path: "/users".to_string(),
1101 operation_id: Some("getUsers".to_string()),
1102 summary: None,
1103 description: None,
1104 parameters: vec![],
1105 },
1106 RouteInfo {
1107 method: "POST".to_string(),
1108 path: "/users".to_string(),
1109 operation_id: Some("createUser".to_string()),
1110 summary: None,
1111 description: None,
1112 parameters: vec![],
1113 },
1114 ];
1115
1116 let state = HttpServerState::with_routes(routes.clone());
1117 assert_eq!(state.routes.len(), 2);
1118 assert_eq!(state.routes[0].method, "GET");
1119 assert_eq!(state.routes[1].method, "POST");
1120}
1121
1122#[test]
1123fn test_http_server_state_clone() {
1124 let routes = vec![RouteInfo {
1125 method: "GET".to_string(),
1126 path: "/test".to_string(),
1127 operation_id: None,
1128 summary: None,
1129 description: None,
1130 parameters: vec![],
1131 }];
1132
1133 let state = HttpServerState::with_routes(routes);
1134 let cloned = state.clone();
1135
1136 assert_eq!(state.routes.len(), cloned.routes.len());
1137 assert_eq!(state.routes[0].method, cloned.routes[0].method);
1138}
1139
1140#[tokio::test]
1141async fn test_build_router_without_openapi() {
1142 let _router = build_router(None, None, None).await;
1143 }
1145
1146#[tokio::test]
1147async fn test_build_router_with_nonexistent_spec() {
1148 let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
1149 }
1151
1152#[tokio::test]
1153async fn test_build_router_with_auth_and_latency() {
1154 let _router = build_router_with_auth_and_latency(None, None, None, None).await;
1155 }
1157
1158#[tokio::test]
1159async fn test_build_router_with_latency() {
1160 let _router = build_router_with_latency(None, None, None).await;
1161 }
1163
1164#[tokio::test]
1165async fn test_build_router_with_auth() {
1166 let _router = build_router_with_auth(None, None, None).await;
1167 }
1169
1170#[tokio::test]
1171async fn test_build_router_with_chains() {
1172 let _router = build_router_with_chains(None, None, None).await;
1173 }
1175
1176#[test]
1177fn test_route_info_with_all_fields() {
1178 let route = RouteInfo {
1179 method: "PUT".to_string(),
1180 path: "/users/{id}".to_string(),
1181 operation_id: Some("updateUser".to_string()),
1182 summary: Some("Update user".to_string()),
1183 description: Some("Updates an existing user".to_string()),
1184 parameters: vec!["id".to_string(), "body".to_string()],
1185 };
1186
1187 assert!(route.operation_id.is_some());
1188 assert!(route.summary.is_some());
1189 assert!(route.description.is_some());
1190 assert_eq!(route.parameters.len(), 2);
1191}
1192
1193#[test]
1194fn test_route_info_with_minimal_fields() {
1195 let route = RouteInfo {
1196 method: "DELETE".to_string(),
1197 path: "/users/{id}".to_string(),
1198 operation_id: None,
1199 summary: None,
1200 description: None,
1201 parameters: vec![],
1202 };
1203
1204 assert!(route.operation_id.is_none());
1205 assert!(route.summary.is_none());
1206 assert!(route.description.is_none());
1207 assert_eq!(route.parameters.len(), 0);
1208}
1209
1210#[test]
1211fn test_http_server_state_empty_routes() {
1212 let state = HttpServerState::with_routes(vec![]);
1213 assert_eq!(state.routes.len(), 0);
1214}
1215
1216#[test]
1217fn test_http_server_state_multiple_routes() {
1218 let routes = vec![
1219 RouteInfo {
1220 method: "GET".to_string(),
1221 path: "/users".to_string(),
1222 operation_id: Some("listUsers".to_string()),
1223 summary: Some("List all users".to_string()),
1224 description: None,
1225 parameters: vec![],
1226 },
1227 RouteInfo {
1228 method: "GET".to_string(),
1229 path: "/users/{id}".to_string(),
1230 operation_id: Some("getUser".to_string()),
1231 summary: Some("Get a user".to_string()),
1232 description: None,
1233 parameters: vec!["id".to_string()],
1234 },
1235 RouteInfo {
1236 method: "POST".to_string(),
1237 path: "/users".to_string(),
1238 operation_id: Some("createUser".to_string()),
1239 summary: Some("Create a user".to_string()),
1240 description: None,
1241 parameters: vec!["body".to_string()],
1242 },
1243 ];
1244
1245 let state = HttpServerState::with_routes(routes);
1246 assert_eq!(state.routes.len(), 3);
1247
1248 let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
1250 assert!(methods.contains(&"GET"));
1251 assert!(methods.contains(&"POST"));
1252}
1253
1254#[test]
1255fn test_http_server_state_with_rate_limiter() {
1256 use std::sync::Arc;
1257
1258 let config = crate::middleware::RateLimitConfig::default();
1259 let rate_limiter = Arc::new(crate::middleware::GlobalRateLimiter::new(config));
1260
1261 let state = HttpServerState::new().with_rate_limiter(rate_limiter);
1262
1263 assert!(state.rate_limiter.is_some());
1264 assert_eq!(state.routes.len(), 0);
1265}
1266
1267#[tokio::test]
1268async fn test_build_router_includes_rate_limiter() {
1269 let _router = build_router(None, None, None).await;
1270 }