mockforge_test/
health.rs

1//! Health check utilities for MockForge servers
2
3use crate::error::{Error, Result};
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7use tokio::time::{interval, timeout};
8use tracing::{debug, trace};
9
10/// Health status response from MockForge server
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct HealthStatus {
13    /// Status of the server ("healthy" or "unhealthy: <reason>")
14    pub status: String,
15
16    /// Timestamp of the health check
17    pub timestamp: String,
18
19    /// Server uptime in seconds
20    pub uptime_seconds: u64,
21
22    /// Server version
23    pub version: String,
24}
25
26impl HealthStatus {
27    /// Check if the server is healthy
28    pub fn is_healthy(&self) -> bool {
29        self.status == "healthy"
30    }
31}
32
33/// Health check client for MockForge servers
34pub struct HealthCheck {
35    client: Client,
36    base_url: String,
37}
38
39impl HealthCheck {
40    /// Create a new health check client
41    ///
42    /// # Arguments
43    ///
44    /// * `host` - Server host (e.g., "localhost")
45    /// * `port` - Server port
46    pub fn new(host: &str, port: u16) -> Self {
47        Self {
48            client: Client::builder()
49                .timeout(Duration::from_secs(5))
50                .build()
51                .expect("Failed to build HTTP client"),
52            base_url: format!("http://{}:{}", host, port),
53        }
54    }
55
56    /// Perform a single health check
57    pub async fn check(&self) -> Result<HealthStatus> {
58        let url = format!("{}/health", self.base_url);
59        trace!("Checking health at: {}", url);
60
61        let response = self.client.get(&url).send().await?;
62
63        if !response.status().is_success() {
64            return Err(Error::HealthCheckFailed(format!(
65                "HTTP {} - {}",
66                response.status(),
67                response.text().await.unwrap_or_default()
68            )));
69        }
70
71        let status: HealthStatus = response.json().await?;
72        debug!("Health check response: {:?}", status);
73
74        Ok(status)
75    }
76
77    /// Wait for the server to become healthy
78    ///
79    /// # Arguments
80    ///
81    /// * `timeout_duration` - Maximum time to wait
82    /// * `check_interval` - Interval between health checks
83    pub async fn wait_until_healthy(
84        &self,
85        timeout_duration: Duration,
86        check_interval: Duration,
87    ) -> Result<HealthStatus> {
88        debug!(
89            "Waiting for server to become healthy (timeout: {:?}, interval: {:?})",
90            timeout_duration, check_interval
91        );
92
93        let check_fut = async {
94            let mut check_timer = interval(check_interval);
95
96            loop {
97                check_timer.tick().await;
98
99                match self.check().await {
100                    Ok(status) => {
101                        if status.is_healthy() {
102                            debug!("Server is healthy!");
103                            return Ok(status);
104                        }
105                        trace!("Server not healthy yet: {}", status.status);
106                    }
107                    Err(e) => {
108                        trace!("Health check failed: {}", e);
109                    }
110                }
111            }
112        };
113
114        timeout(timeout_duration, check_fut)
115            .await
116            .map_err(|_| Error::HealthCheckTimeout(timeout_duration.as_secs()))?
117    }
118
119    /// Check if the server is ready (health endpoint returns 200)
120    pub async fn is_ready(&self) -> bool {
121        self.check().await.is_ok()
122    }
123
124    /// Get the base URL of the server
125    pub fn base_url(&self) -> &str {
126        &self.base_url
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_health_status_is_healthy() {
136        let status = HealthStatus {
137            status: "healthy".to_string(),
138            timestamp: "2024-01-01T00:00:00Z".to_string(),
139            uptime_seconds: 10,
140            version: "0.1.0".to_string(),
141        };
142
143        assert!(status.is_healthy());
144    }
145
146    #[test]
147    fn test_health_status_is_not_healthy() {
148        let status = HealthStatus {
149            status: "unhealthy: database connection failed".to_string(),
150            timestamp: "2024-01-01T00:00:00Z".to_string(),
151            uptime_seconds: 10,
152            version: "0.1.0".to_string(),
153        };
154
155        assert!(!status.is_healthy());
156    }
157
158    #[test]
159    fn test_health_check_creation() {
160        let health = HealthCheck::new("localhost", 3000);
161        assert_eq!(health.base_url(), "http://localhost:3000");
162    }
163}