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