Skip to main content

mockforge_http/
health.rs

1//! Kubernetes-native health check endpoints
2//!
3//! This module provides comprehensive health check endpoints following Kubernetes best practices:
4//! - **Liveness probe**: Indicates if the container is alive
5//! - **Readiness probe**: Indicates if the container is ready to accept traffic
6//! - **Startup probe**: Indicates if the container has finished initialization
7//!
8//! These endpoints are essential for:
9//! - Kubernetes deployment orchestration
10//! - Load balancer health checks
11//! - Service discovery integration
12//! - Graceful shutdown coordination
13
14use axum::{extract::State, http::StatusCode, response::Json, routing::get};
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18use tokio::sync::RwLock;
19use tracing::{debug, error, info, warn};
20
21/// Service initialization status
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ServiceStatus {
24    /// Service is initializing (not ready)
25    Initializing,
26    /// Service is ready to accept traffic
27    Ready,
28    /// Service is shutting down (not accepting new requests)
29    ShuttingDown,
30    /// Service has failed and is unhealthy
31    Failed,
32}
33
34impl ServiceStatus {
35    /// Check if service is ready to accept traffic
36    pub fn is_ready(&self) -> bool {
37        matches!(self, ServiceStatus::Ready)
38    }
39
40    /// Check if service is alive (not failed)
41    pub fn is_alive(&self) -> bool {
42        !matches!(self, ServiceStatus::Failed)
43    }
44}
45
46/// Health check manager for tracking service state
47#[derive(Debug, Clone)]
48pub struct HealthManager {
49    /// Current service status
50    status: Arc<RwLock<ServiceStatus>>,
51    /// Server startup time
52    start_time: Arc<Instant>,
53    /// Service initialization deadline (timeout)
54    init_deadline: Arc<Option<Instant>>,
55    /// Shutdown signal for graceful termination
56    shutdown_signal: Arc<RwLock<Option<tokio::sync::oneshot::Sender<()>>>>,
57}
58
59impl HealthManager {
60    /// Create a new health manager
61    pub fn new() -> Self {
62        Self {
63            status: Arc::new(RwLock::new(ServiceStatus::Initializing)),
64            start_time: Arc::new(Instant::now()),
65            init_deadline: Arc::new(None),
66            shutdown_signal: Arc::new(RwLock::new(None)),
67        }
68    }
69
70    /// Create a new health manager with initialization timeout
71    pub fn with_init_timeout(timeout: Duration) -> Self {
72        let deadline = Instant::now() + timeout;
73        Self {
74            status: Arc::new(RwLock::new(ServiceStatus::Initializing)),
75            start_time: Arc::new(Instant::now()),
76            init_deadline: Arc::new(Some(deadline)),
77            shutdown_signal: Arc::new(RwLock::new(None)),
78        }
79    }
80
81    /// Mark service as ready
82    pub async fn set_ready(&self) {
83        let mut status = self.status.write().await;
84        *status = ServiceStatus::Ready;
85        info!("Service marked as ready");
86    }
87
88    /// Mark service as failed
89    pub async fn set_failed(&self, reason: &str) {
90        let mut status = self.status.write().await;
91        *status = ServiceStatus::Failed;
92        error!("Service marked as failed: {}", reason);
93    }
94
95    /// Mark service as shutting down
96    pub async fn set_shutting_down(&self) {
97        let mut status = self.status.write().await;
98        *status = ServiceStatus::ShuttingDown;
99        info!("Service marked as shutting down");
100    }
101
102    /// Get current service status
103    pub async fn get_status(&self) -> ServiceStatus {
104        *self.status.read().await
105    }
106
107    /// Get server uptime in seconds
108    pub fn uptime_seconds(&self) -> u64 {
109        self.start_time.elapsed().as_secs()
110    }
111
112    /// Check if initialization has timed out
113    pub fn is_init_timeout(&self) -> bool {
114        if let Some(deadline) = *self.init_deadline {
115            Instant::now() > deadline
116        } else {
117            false
118        }
119    }
120
121    /// Set shutdown signal receiver for graceful shutdown
122    pub async fn set_shutdown_signal(&self, sender: tokio::sync::oneshot::Sender<()>) {
123        let mut signal = self.shutdown_signal.write().await;
124        *signal = Some(sender);
125    }
126
127    /// Trigger graceful shutdown
128    pub async fn trigger_shutdown(&self) {
129        self.set_shutting_down().await;
130        let mut signal = self.shutdown_signal.write().await;
131        if let Some(sender) = signal.take() {
132            let _ = sender.send(());
133            info!("Graceful shutdown signal sent");
134        }
135    }
136}
137
138impl Default for HealthManager {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144/// Health check response structure
145#[derive(Debug, Serialize, Deserialize)]
146pub struct HealthResponse {
147    /// Service status
148    pub status: String,
149    /// ISO 8601 timestamp
150    pub timestamp: String,
151    /// Server uptime in seconds
152    pub uptime_seconds: u64,
153    /// Service version
154    pub version: String,
155    /// Additional status information
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub details: Option<HealthDetails>,
158}
159
160/// Detailed health information
161#[derive(Debug, Serialize, Deserialize)]
162pub struct HealthDetails {
163    /// Service initialization status
164    pub initialization: String,
165    /// Active connection count (if available)
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub connections: Option<u64>,
168    /// Memory usage in bytes (if available)
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub memory_bytes: Option<u64>,
171}
172
173/// Liveness probe endpoint
174///
175/// Kubernetes uses this to determine if the container should be restarted.
176/// Returns 200 if the service is alive, 503 if it has failed.
177///
178/// This should be a lightweight check that doesn't depend on external services.
179async fn liveness_probe(
180    State(health): State<Arc<HealthManager>>,
181) -> Result<Json<HealthResponse>, StatusCode> {
182    let status = health.get_status().await;
183    let uptime = health.uptime_seconds();
184
185    // Liveness checks if the process is alive (not failed)
186    if status.is_alive() {
187        let response = HealthResponse {
188            status: "alive".to_string(),
189            timestamp: chrono::Utc::now().to_rfc3339(),
190            uptime_seconds: uptime,
191            version: env!("CARGO_PKG_VERSION").to_string(),
192            details: None,
193        };
194        Ok(Json(response))
195    } else {
196        Err(StatusCode::SERVICE_UNAVAILABLE)
197    }
198}
199
200/// Readiness probe endpoint
201///
202/// Kubernetes uses this to determine if the container is ready to receive traffic.
203/// Returns 200 if the service is ready, 503 if it's not ready or shutting down.
204///
205/// This checks if the service has completed initialization and is ready to serve requests.
206async fn readiness_probe(
207    State(health): State<Arc<HealthManager>>,
208) -> Result<Json<HealthResponse>, (StatusCode, Json<HealthResponse>)> {
209    let status = health.get_status().await;
210    let uptime = health.uptime_seconds();
211
212    // Readiness checks if the service is ready to accept traffic
213    if status.is_ready() {
214        let response = HealthResponse {
215            status: "ready".to_string(),
216            timestamp: chrono::Utc::now().to_rfc3339(),
217            uptime_seconds: uptime,
218            version: env!("CARGO_PKG_VERSION").to_string(),
219            details: Some(HealthDetails {
220                initialization: "complete".to_string(),
221                connections: None,
222                memory_bytes: None,
223            }),
224        };
225        Ok(Json(response))
226    } else {
227        let details = match status {
228            ServiceStatus::Initializing => {
229                if health.is_init_timeout() {
230                    "initialization_timeout".to_string()
231                } else {
232                    "initializing".to_string()
233                }
234            }
235            ServiceStatus::ShuttingDown => "shutting_down".to_string(),
236            ServiceStatus::Failed => "failed".to_string(),
237            ServiceStatus::Ready => unreachable!(),
238        };
239
240        let response = HealthResponse {
241            status: "not_ready".to_string(),
242            timestamp: chrono::Utc::now().to_rfc3339(),
243            uptime_seconds: uptime,
244            version: env!("CARGO_PKG_VERSION").to_string(),
245            details: Some(HealthDetails {
246                initialization: details,
247                connections: None,
248                memory_bytes: None,
249            }),
250        };
251
252        Err((StatusCode::SERVICE_UNAVAILABLE, Json(response)))
253    }
254}
255
256/// Startup probe endpoint
257///
258/// Kubernetes uses this to determine if the container has finished initialization.
259/// This is useful for services that take a long time to start.
260/// Returns 200 once initialization is complete, 503 while still initializing.
261async fn startup_probe(
262    State(health): State<Arc<HealthManager>>,
263) -> Result<Json<HealthResponse>, StatusCode> {
264    let status = health.get_status().await;
265    let uptime = health.uptime_seconds();
266
267    // Startup probe checks if initialization is complete
268    match status {
269        ServiceStatus::Ready => {
270            let response = HealthResponse {
271                status: "startup_complete".to_string(),
272                timestamp: chrono::Utc::now().to_rfc3339(),
273                uptime_seconds: uptime,
274                version: env!("CARGO_PKG_VERSION").to_string(),
275                details: Some(HealthDetails {
276                    initialization: "complete".to_string(),
277                    connections: None,
278                    memory_bytes: None,
279                }),
280            };
281            Ok(Json(response))
282        }
283        ServiceStatus::Initializing => {
284            if health.is_init_timeout() {
285                warn!("Startup probe: initialization timeout exceeded");
286                Err(StatusCode::SERVICE_UNAVAILABLE)
287            } else {
288                debug!("Startup probe: still initializing");
289                Err(StatusCode::SERVICE_UNAVAILABLE)
290            }
291        }
292        ServiceStatus::Failed => Err(StatusCode::SERVICE_UNAVAILABLE),
293        ServiceStatus::ShuttingDown => {
294            // During shutdown, startup probe should return ready (service was started)
295            let response = HealthResponse {
296                status: "startup_complete".to_string(),
297                timestamp: chrono::Utc::now().to_rfc3339(),
298                uptime_seconds: uptime,
299                version: env!("CARGO_PKG_VERSION").to_string(),
300                details: Some(HealthDetails {
301                    initialization: "complete".to_string(),
302                    connections: None,
303                    memory_bytes: None,
304                }),
305            };
306            Ok(Json(response))
307        }
308    }
309}
310
311/// Combined health check endpoint (backwards compatibility)
312///
313/// This endpoint provides a general health check that combines liveness and readiness.
314/// For Kubernetes deployments, prefer using the specific probe endpoints.
315async fn health_check(
316    State(health): State<Arc<HealthManager>>,
317) -> Result<Json<HealthResponse>, (StatusCode, Json<HealthResponse>)> {
318    let status = health.get_status().await;
319    let uptime = health.uptime_seconds();
320
321    if status.is_ready() {
322        let response = HealthResponse {
323            status: "healthy".to_string(),
324            timestamp: chrono::Utc::now().to_rfc3339(),
325            uptime_seconds: uptime,
326            version: env!("CARGO_PKG_VERSION").to_string(),
327            details: Some(HealthDetails {
328                initialization: "complete".to_string(),
329                connections: None,
330                memory_bytes: None,
331            }),
332        };
333        Ok(Json(response))
334    } else {
335        let status_str = match status {
336            ServiceStatus::Initializing => "initializing",
337            ServiceStatus::ShuttingDown => "shutting_down",
338            ServiceStatus::Failed => "failed",
339            ServiceStatus::Ready => unreachable!(),
340        };
341
342        let response = HealthResponse {
343            status: status_str.to_string(),
344            timestamp: chrono::Utc::now().to_rfc3339(),
345            uptime_seconds: uptime,
346            version: env!("CARGO_PKG_VERSION").to_string(),
347            details: Some(HealthDetails {
348                initialization: status_str.to_string(),
349                connections: None,
350                memory_bytes: None,
351            }),
352        };
353
354        Err((StatusCode::SERVICE_UNAVAILABLE, Json(response)))
355    }
356}
357
358/// Create health check router with all probe endpoints
359pub fn health_router(health_manager: Arc<HealthManager>) -> axum::Router {
360    health_router_with_exclusions(health_manager, &[])
361}
362
363/// Paths served by the built-in health probes.
364pub(super) const HEALTH_PATHS: [&str; 4] = [
365    "/health",
366    "/health/live",
367    "/health/ready",
368    "/health/startup",
369];
370
371/// Register only probes that do not overlap an explicit mock handler.
372pub(super) fn health_router_with_exclusions(
373    health_manager: Arc<HealthManager>,
374    excluded_paths: &[&str],
375) -> axum::Router {
376    use axum::Router;
377    let mut app = Router::new();
378    let handlers = [
379        get(health_check),
380        get(liveness_probe),
381        get(readiness_probe),
382        get(startup_probe),
383    ];
384    for (path, handler) in HEALTH_PATHS.into_iter().zip(handlers) {
385        if !excluded_paths.contains(&path) {
386            app = app.route(path, handler);
387        }
388    }
389    app.with_state(health_manager)
390}
391
392/// Create health check router with custom prefix
393pub fn health_router_with_prefix(health_manager: Arc<HealthManager>, prefix: &str) -> axum::Router {
394    use axum::Router;
395    Router::new()
396        .route(&format!("{}/health", prefix), get(health_check))
397        .route(&format!("{}/health/live", prefix), get(liveness_probe))
398        .route(&format!("{}/health/ready", prefix), get(readiness_probe))
399        .route(&format!("{}/health/startup", prefix), get(startup_probe))
400        .with_state(health_manager)
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use axum::body::Body;
407    use axum::http::Request;
408    use tower::ServiceExt;
409
410    // ==================== ServiceStatus Tests ====================
411
412    #[test]
413    fn test_service_status() {
414        assert!(ServiceStatus::Ready.is_ready());
415        assert!(!ServiceStatus::Initializing.is_ready());
416        assert!(!ServiceStatus::ShuttingDown.is_ready());
417        assert!(!ServiceStatus::Failed.is_ready());
418
419        assert!(ServiceStatus::Ready.is_alive());
420        assert!(ServiceStatus::Initializing.is_alive());
421        assert!(ServiceStatus::ShuttingDown.is_alive());
422        assert!(!ServiceStatus::Failed.is_alive());
423    }
424
425    #[test]
426    fn test_service_status_eq() {
427        assert_eq!(ServiceStatus::Ready, ServiceStatus::Ready);
428        assert_ne!(ServiceStatus::Ready, ServiceStatus::Failed);
429        assert_eq!(ServiceStatus::Initializing, ServiceStatus::Initializing);
430    }
431
432    #[test]
433    fn test_service_status_clone() {
434        let status = ServiceStatus::Ready;
435        let cloned = status;
436        assert_eq!(status, cloned);
437    }
438
439    #[test]
440    fn test_service_status_debug() {
441        let debug = format!("{:?}", ServiceStatus::Ready);
442        assert!(debug.contains("Ready"));
443    }
444
445    // ==================== HealthManager Tests ====================
446
447    #[tokio::test]
448    async fn test_health_manager_new() {
449        let manager = HealthManager::new();
450        let status = manager.get_status().await;
451        assert_eq!(status, ServiceStatus::Initializing);
452    }
453
454    #[tokio::test]
455    async fn test_health_manager_default() {
456        let manager = HealthManager::default();
457        let status = manager.get_status().await;
458        assert_eq!(status, ServiceStatus::Initializing);
459    }
460
461    #[tokio::test]
462    async fn test_health_manager_with_init_timeout() {
463        let manager = HealthManager::with_init_timeout(Duration::from_secs(30));
464        let status = manager.get_status().await;
465        assert_eq!(status, ServiceStatus::Initializing);
466        assert!(!manager.is_init_timeout());
467    }
468
469    #[tokio::test]
470    async fn test_health_manager_set_ready() {
471        let manager = HealthManager::new();
472        manager.set_ready().await;
473        let status = manager.get_status().await;
474        assert_eq!(status, ServiceStatus::Ready);
475    }
476
477    #[tokio::test]
478    async fn test_health_manager_set_failed() {
479        let manager = HealthManager::new();
480        manager.set_failed("test error").await;
481        let status = manager.get_status().await;
482        assert_eq!(status, ServiceStatus::Failed);
483    }
484
485    #[tokio::test]
486    async fn test_health_manager_set_shutting_down() {
487        let manager = HealthManager::new();
488        manager.set_shutting_down().await;
489        let status = manager.get_status().await;
490        assert_eq!(status, ServiceStatus::ShuttingDown);
491    }
492
493    #[tokio::test]
494    async fn test_health_manager_uptime() {
495        let manager = HealthManager::new();
496        let uptime = manager.uptime_seconds();
497        // Uptime should be very small immediately after creation
498        assert!(uptime < 5);
499    }
500
501    #[tokio::test]
502    async fn test_health_manager_clone() {
503        let manager = HealthManager::new();
504        manager.set_ready().await;
505        let cloned = manager.clone();
506        let status = cloned.get_status().await;
507        assert_eq!(status, ServiceStatus::Ready);
508    }
509
510    #[tokio::test]
511    async fn test_health_manager_trigger_shutdown() {
512        let manager = HealthManager::new();
513        manager.set_ready().await;
514        manager.trigger_shutdown().await;
515        let status = manager.get_status().await;
516        assert_eq!(status, ServiceStatus::ShuttingDown);
517    }
518
519    // ==================== HealthResponse Tests ====================
520
521    #[test]
522    fn test_health_response_serialization() {
523        let response = HealthResponse {
524            status: "healthy".to_string(),
525            timestamp: "2024-01-15T10:30:00Z".to_string(),
526            uptime_seconds: 3600,
527            version: "1.0.0".to_string(),
528            details: None,
529        };
530
531        let json = serde_json::to_string(&response).unwrap();
532        assert!(json.contains("healthy"));
533        assert!(json.contains("3600"));
534        assert!(json.contains("1.0.0"));
535    }
536
537    #[test]
538    fn test_health_response_with_details() {
539        let response = HealthResponse {
540            status: "healthy".to_string(),
541            timestamp: "2024-01-15T10:30:00Z".to_string(),
542            uptime_seconds: 3600,
543            version: "1.0.0".to_string(),
544            details: Some(HealthDetails {
545                initialization: "complete".to_string(),
546                connections: Some(10),
547                memory_bytes: Some(1024 * 1024),
548            }),
549        };
550
551        let json = serde_json::to_string(&response).unwrap();
552        assert!(json.contains("complete"));
553        assert!(json.contains("connections"));
554    }
555
556    #[test]
557    fn test_health_response_deserialization() {
558        let json = r#"{
559            "status": "healthy",
560            "timestamp": "2024-01-15T10:30:00Z",
561            "uptime_seconds": 7200,
562            "version": "2.0.0"
563        }"#;
564
565        let response: HealthResponse = serde_json::from_str(json).unwrap();
566        assert_eq!(response.status, "healthy");
567        assert_eq!(response.uptime_seconds, 7200);
568        assert_eq!(response.version, "2.0.0");
569    }
570
571    // ==================== HealthDetails Tests ====================
572
573    #[test]
574    fn test_health_details_serialization() {
575        let details = HealthDetails {
576            initialization: "complete".to_string(),
577            connections: Some(5),
578            memory_bytes: Some(2048),
579        };
580
581        let json = serde_json::to_string(&details).unwrap();
582        assert!(json.contains("complete"));
583        assert!(json.contains("5"));
584        assert!(json.contains("2048"));
585    }
586
587    #[test]
588    fn test_health_details_optional_fields() {
589        let details = HealthDetails {
590            initialization: "initializing".to_string(),
591            connections: None,
592            memory_bytes: None,
593        };
594
595        let json = serde_json::to_string(&details).unwrap();
596        assert!(json.contains("initializing"));
597        assert!(!json.contains("connections"));
598        assert!(!json.contains("memory_bytes"));
599    }
600
601    // ==================== Probe Endpoint Tests ====================
602
603    #[tokio::test]
604    async fn test_liveness_probe_alive() {
605        let health = Arc::new(HealthManager::new());
606        health.set_ready().await;
607
608        let app = health_router(health.clone());
609        let response = app
610            .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
611            .await
612            .unwrap();
613
614        assert_eq!(response.status(), StatusCode::OK);
615    }
616
617    #[tokio::test]
618    async fn test_liveness_probe_failed() {
619        let health = Arc::new(HealthManager::new());
620        health.set_failed("test failure").await;
621
622        let app = health_router(health.clone());
623        let response = app
624            .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
625            .await
626            .unwrap();
627
628        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
629    }
630
631    #[tokio::test]
632    async fn test_liveness_probe_initializing() {
633        let health = Arc::new(HealthManager::new());
634        // Initializing is still alive
635
636        let app = health_router(health.clone());
637        let response = app
638            .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
639            .await
640            .unwrap();
641
642        assert_eq!(response.status(), StatusCode::OK);
643    }
644
645    #[tokio::test]
646    async fn test_liveness_probe_shutting_down() {
647        let health = Arc::new(HealthManager::new());
648        health.set_shutting_down().await;
649
650        let app = health_router(health.clone());
651        let response = app
652            .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
653            .await
654            .unwrap();
655
656        // Shutting down is still alive
657        assert_eq!(response.status(), StatusCode::OK);
658    }
659
660    #[tokio::test]
661    async fn test_readiness_probe_ready() {
662        let health = Arc::new(HealthManager::new());
663        health.set_ready().await;
664
665        let app = health_router(health.clone());
666        let response = app
667            .oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
668            .await
669            .unwrap();
670
671        assert_eq!(response.status(), StatusCode::OK);
672    }
673
674    #[tokio::test]
675    async fn test_readiness_probe_initializing() {
676        let health = Arc::new(HealthManager::new());
677
678        let app = health_router(health.clone());
679        let response = app
680            .oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
681            .await
682            .unwrap();
683
684        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
685    }
686
687    #[tokio::test]
688    async fn test_readiness_probe_shutting_down() {
689        let health = Arc::new(HealthManager::new());
690        health.set_shutting_down().await;
691
692        let app = health_router(health.clone());
693        let response = app
694            .oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
695            .await
696            .unwrap();
697
698        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
699    }
700
701    #[tokio::test]
702    async fn test_startup_probe_ready() {
703        let health = Arc::new(HealthManager::new());
704        health.set_ready().await;
705
706        let app = health_router(health.clone());
707        let response = app
708            .oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
709            .await
710            .unwrap();
711
712        assert_eq!(response.status(), StatusCode::OK);
713    }
714
715    #[tokio::test]
716    async fn test_startup_probe_initializing() {
717        let health = Arc::new(HealthManager::new());
718
719        let app = health_router(health.clone());
720        let response = app
721            .oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
722            .await
723            .unwrap();
724
725        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
726    }
727
728    #[tokio::test]
729    async fn test_startup_probe_shutting_down() {
730        let health = Arc::new(HealthManager::new());
731        health.set_ready().await;
732        health.set_shutting_down().await;
733
734        let app = health_router(health.clone());
735        let response = app
736            .oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
737            .await
738            .unwrap();
739
740        // During shutdown, startup probe returns OK (service was started)
741        assert_eq!(response.status(), StatusCode::OK);
742    }
743
744    #[tokio::test]
745    async fn test_health_check_ready() {
746        let health = Arc::new(HealthManager::new());
747        health.set_ready().await;
748
749        let app = health_router(health.clone());
750        let response = app
751            .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
752            .await
753            .unwrap();
754
755        assert_eq!(response.status(), StatusCode::OK);
756    }
757
758    #[tokio::test]
759    async fn test_health_check_not_ready() {
760        let health = Arc::new(HealthManager::new());
761
762        let app = health_router(health.clone());
763        let response = app
764            .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
765            .await
766            .unwrap();
767
768        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
769    }
770
771    // ==================== Router Tests ====================
772
773    #[test]
774    fn test_health_router_creation() {
775        let health = Arc::new(HealthManager::new());
776        let router = health_router(health);
777        let _ = router;
778    }
779
780    #[test]
781    fn test_health_router_with_prefix() {
782        let health = Arc::new(HealthManager::new());
783        let router = health_router_with_prefix(health, "/api/v1");
784        let _ = router;
785    }
786}