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