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