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    fn create_healthy_status() -> HealthStatus {
135        HealthStatus {
136            status: "healthy".to_string(),
137            timestamp: "2024-01-01T00:00:00Z".to_string(),
138            uptime_seconds: 10,
139            version: "0.1.0".to_string(),
140        }
141    }
142
143    fn create_unhealthy_status(reason: &str) -> HealthStatus {
144        HealthStatus {
145            status: format!("unhealthy: {}", reason),
146            timestamp: "2024-01-01T00:00:00Z".to_string(),
147            uptime_seconds: 10,
148            version: "0.1.0".to_string(),
149        }
150    }
151
152    // HealthStatus tests
153    #[test]
154    fn test_health_status_is_healthy() {
155        let status = create_healthy_status();
156        assert!(status.is_healthy());
157    }
158
159    #[test]
160    fn test_health_status_is_not_healthy() {
161        let status = create_unhealthy_status("database connection failed");
162        assert!(!status.is_healthy());
163    }
164
165    #[test]
166    fn test_health_status_empty_status_not_healthy() {
167        let status = HealthStatus {
168            status: "".to_string(),
169            timestamp: "2024-01-01T00:00:00Z".to_string(),
170            uptime_seconds: 0,
171            version: "0.1.0".to_string(),
172        };
173        assert!(!status.is_healthy());
174    }
175
176    #[test]
177    fn test_health_status_clone() {
178        let status = create_healthy_status();
179        let cloned = status.clone();
180        assert_eq!(status.status, cloned.status);
181        assert_eq!(status.uptime_seconds, cloned.uptime_seconds);
182        assert_eq!(status.version, cloned.version);
183    }
184
185    #[test]
186    fn test_health_status_debug() {
187        let status = create_healthy_status();
188        let debug = format!("{:?}", status);
189        assert!(debug.contains("HealthStatus"));
190        assert!(debug.contains("healthy"));
191    }
192
193    #[test]
194    fn test_health_status_serialize() {
195        let status = create_healthy_status();
196        let json = serde_json::to_string(&status).unwrap();
197        assert!(json.contains("\"status\":\"healthy\""));
198        assert!(json.contains("\"uptime_seconds\":10"));
199        assert!(json.contains("\"version\":\"0.1.0\""));
200    }
201
202    #[test]
203    fn test_health_status_deserialize() {
204        let json = r#"{
205            "status": "healthy",
206            "timestamp": "2025-01-01T12:00:00Z",
207            "uptime_seconds": 3600,
208            "version": "1.0.0"
209        }"#;
210
211        let status: HealthStatus = serde_json::from_str(json).unwrap();
212        assert_eq!(status.status, "healthy");
213        assert_eq!(status.uptime_seconds, 3600);
214        assert_eq!(status.version, "1.0.0");
215        assert!(status.is_healthy());
216    }
217
218    #[test]
219    fn test_health_status_with_long_uptime() {
220        let status = HealthStatus {
221            status: "healthy".to_string(),
222            timestamp: "2024-01-01T00:00:00Z".to_string(),
223            uptime_seconds: 86400 * 365, // 1 year
224            version: "0.1.0".to_string(),
225        };
226        assert!(status.is_healthy());
227        assert_eq!(status.uptime_seconds, 31_536_000);
228    }
229
230    // HealthCheck tests
231    #[test]
232    fn test_health_check_creation() {
233        let health = HealthCheck::new("localhost", 3000);
234        assert_eq!(health.base_url(), "http://localhost:3000");
235    }
236
237    #[test]
238    fn test_health_check_creation_different_host() {
239        let health = HealthCheck::new("192.168.1.100", 8080);
240        assert_eq!(health.base_url(), "http://192.168.1.100:8080");
241    }
242
243    #[test]
244    fn test_health_check_creation_with_hostname() {
245        let health = HealthCheck::new("api.example.com", 443);
246        assert_eq!(health.base_url(), "http://api.example.com:443");
247    }
248
249    #[test]
250    fn test_health_check_base_url_method() {
251        let health = HealthCheck::new("test-server", 9000);
252        let url = health.base_url();
253        assert_eq!(url, "http://test-server:9000");
254    }
255}