Skip to main content

rust_serv/management/
handler.rs

1//! Management API handler
2//!
3//! This module provides handlers for management API endpoints.
4
5use super::config::ManagementConfig;
6use super::stats::StatsCollector;
7use super::json_response;
8use http_body_util::Full;
9use hyper::body::Bytes;
10use hyper::{Request, Response};
11use serde::{Deserialize, Serialize};
12
13/// Management API response wrapper
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct ManagementResponse {
16    /// Response status
17    pub status: String,
18}
19
20/// Management API handler
21#[derive(Debug, Clone)]
22pub struct ManagementHandler {
23    config: ManagementConfig,
24    stats: StatsCollector,
25}
26
27impl ManagementHandler {
28    /// Create a new management handler
29    pub fn new(config: ManagementConfig, stats: StatsCollector) -> Self {
30        Self { config, stats }
31    }
32
33    /// Create a new management handler with default stats collector
34    pub fn with_config(config: ManagementConfig) -> Self {
35        Self {
36            config,
37            stats: StatsCollector::new(),
38        }
39    }
40
41    /// Get the stats collector
42    pub fn stats(&self) -> &StatsCollector {
43        &self.stats
44    }
45
46    /// Get a clone of the stats collector
47    pub fn stats_collector(&self) -> StatsCollector {
48        self.stats.clone()
49    }
50
51    /// Check if a path matches a management endpoint
52    pub fn is_management_path(&self, path: &str) -> bool {
53        if !self.config.enabled {
54            return false;
55        }
56        path == self.config.health_path
57            || path == self.config.ready_path
58            || path == self.config.stats_path
59    }
60
61    /// Handle a management API request
62    pub fn handle_request(
63        &self,
64        req: &Request<hyper::body::Incoming>,
65    ) -> Option<Response<Full<Bytes>>> {
66        if !self.config.enabled {
67            return None;
68        }
69
70        let path = req.uri().path();
71
72        if path == self.config.health_path {
73            Some(self.handle_health())
74        } else if path == self.config.ready_path {
75            Some(self.handle_ready())
76        } else if path == self.config.stats_path {
77            Some(self.handle_stats())
78        } else {
79            None
80        }
81    }
82
83    /// Handle health check request
84    /// GET /health -> 200 OK {"status":"healthy"}
85    pub fn handle_health(&self) -> Response<Full<Bytes>> {
86        let response = ManagementResponse {
87            status: "healthy".to_string(),
88        };
89        json_response(200, &serde_json::to_string(&response).unwrap())
90    }
91
92    /// Handle readiness check request
93    /// GET /ready -> 200 OK {"status":"ready"} or 503 {"status":"not_ready"}
94    pub fn handle_ready(&self) -> Response<Full<Bytes>> {
95        if self.stats.is_ready() {
96            let response = ManagementResponse {
97                status: "ready".to_string(),
98            };
99            json_response(200, &serde_json::to_string(&response).unwrap())
100        } else {
101            let response = ManagementResponse {
102                status: "not_ready".to_string(),
103            };
104            json_response(503, &serde_json::to_string(&response).unwrap())
105        }
106    }
107
108    /// Handle stats request
109    /// GET /stats -> JSON statistics
110    pub fn handle_stats(&self) -> Response<Full<Bytes>> {
111        let stats = self.stats.get_stats();
112        json_response(200, &serde_json::to_string(&stats).unwrap())
113    }
114
115    /// Get the health path
116    pub fn health_path(&self) -> &str {
117        &self.config.health_path
118    }
119
120    /// Get the ready path
121    pub fn ready_path(&self) -> &str {
122        &self.config.ready_path
123    }
124
125    /// Get the stats path
126    pub fn stats_path(&self) -> &str {
127        &self.config.stats_path
128    }
129
130    /// Check if management API is enabled
131    pub fn is_enabled(&self) -> bool {
132        self.config.enabled
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use http_body_util::BodyExt;
140
141    // Skip request-based tests - test handler methods directly
142    // Creating hyper::body::Incoming for tests is complex
143    // The handle_request method is tested via integration tests
144
145    #[test]
146    fn test_management_handler_creation() {
147        let config = ManagementConfig {
148            enabled: true,
149            health_path: "/health".to_string(),
150            ready_path: "/ready".to_string(),
151            stats_path: "/stats".to_string(),
152        };
153        let handler = ManagementHandler::with_config(config.clone());
154        assert!(handler.is_enabled());
155        assert_eq!(handler.health_path(), "/health");
156        assert_eq!(handler.ready_path(), "/ready");
157        assert_eq!(handler.stats_path(), "/stats");
158    }
159
160    #[test]
161    fn test_handle_health() {
162        let config = ManagementConfig {
163            enabled: true,
164            health_path: "/health".to_string(),
165            ready_path: "/ready".to_string(),
166            stats_path: "/stats".to_string(),
167        };
168        let handler = ManagementHandler::with_config(config);
169
170        let response = handler.handle_health();
171        assert_eq!(response.status(), 200);
172
173        let body = response.into_body();
174        let bytes = futures::executor::block_on(body.collect()).unwrap().to_bytes();
175        let body_str = String::from_utf8(bytes.to_vec()).unwrap();
176        assert!(body_str.contains("healthy"));
177    }
178
179    #[test]
180    fn test_handle_ready_when_ready() {
181        let config = ManagementConfig {
182            enabled: true,
183            health_path: "/health".to_string(),
184            ready_path: "/ready".to_string(),
185            stats_path: "/stats".to_string(),
186        };
187        let handler = ManagementHandler::with_config(config);
188        handler.stats().set_ready(true);
189
190        let response = handler.handle_ready();
191        assert_eq!(response.status(), 200);
192
193        let body = response.into_body();
194        let bytes = futures::executor::block_on(body.collect()).unwrap().to_bytes();
195        let body_str = String::from_utf8(bytes.to_vec()).unwrap();
196        assert!(body_str.contains("ready"));
197    }
198
199    #[test]
200    fn test_handle_ready_when_not_ready() {
201        let config = ManagementConfig {
202            enabled: true,
203            health_path: "/health".to_string(),
204            ready_path: "/ready".to_string(),
205            stats_path: "/stats".to_string(),
206        };
207        let handler = ManagementHandler::with_config(config);
208        handler.stats().set_ready(false);
209
210        let response = handler.handle_ready();
211        assert_eq!(response.status(), 503);
212
213        let body = response.into_body();
214        let bytes = futures::executor::block_on(body.collect()).unwrap().to_bytes();
215        let body_str = String::from_utf8(bytes.to_vec()).unwrap();
216        assert!(body_str.contains("not_ready"));
217    }
218
219    #[test]
220    fn test_handle_stats() {
221        let config = ManagementConfig {
222            enabled: true,
223            health_path: "/health".to_string(),
224            ready_path: "/ready".to_string(),
225            stats_path: "/stats".to_string(),
226        };
227        let handler = ManagementHandler::with_config(config);
228        handler.stats().increment_requests();
229        handler.stats().increment_connections();
230
231        let response = handler.handle_stats();
232        assert_eq!(response.status(), 200);
233
234        let body = response.into_body();
235        let bytes = futures::executor::block_on(body.collect()).unwrap().to_bytes();
236        let body_str = String::from_utf8(bytes.to_vec()).unwrap();
237        assert!(body_str.contains("total_requests"));
238        assert!(body_str.contains("active_connections"));
239    }
240
241    #[test]
242    fn test_is_management_path_enabled() {
243        let config = ManagementConfig {
244            enabled: true,
245            health_path: "/health".to_string(),
246            ready_path: "/ready".to_string(),
247            stats_path: "/stats".to_string(),
248        };
249        let handler = ManagementHandler::with_config(config);
250
251        assert!(handler.is_management_path("/health"));
252        assert!(handler.is_management_path("/ready"));
253        assert!(handler.is_management_path("/stats"));
254        assert!(!handler.is_management_path("/other"));
255    }
256
257    #[test]
258    fn test_is_management_path_disabled() {
259        let config = ManagementConfig {
260            enabled: false,
261            health_path: "/health".to_string(),
262            ready_path: "/ready".to_string(),
263            stats_path: "/stats".to_string(),
264        };
265        let handler = ManagementHandler::with_config(config);
266
267        assert!(!handler.is_management_path("/health"));
268        assert!(!handler.is_management_path("/ready"));
269        assert!(!handler.is_management_path("/stats"));
270    }
271
272    // Note: handle_request tests require creating hyper::body::Incoming
273    // which is complex in unit tests. These are covered by integration tests.
274    // The individual handler methods (handle_health, handle_ready, handle_stats)
275    // are tested above.
276
277    #[test]
278    fn test_custom_paths() {
279        let config = ManagementConfig {
280            enabled: true,
281            health_path: "/api/health".to_string(),
282            ready_path: "/api/ready".to_string(),
283            stats_path: "/api/stats".to_string(),
284        };
285        let handler = ManagementHandler::with_config(config);
286
287        assert!(handler.is_management_path("/api/health"));
288        assert!(handler.is_management_path("/api/ready"));
289        assert!(handler.is_management_path("/api/stats"));
290        assert!(!handler.is_management_path("/health"));
291    }
292
293    #[test]
294    fn test_handler_clone() {
295        let config = ManagementConfig {
296            enabled: true,
297            health_path: "/health".to_string(),
298            ready_path: "/ready".to_string(),
299            stats_path: "/stats".to_string(),
300        };
301        let handler = ManagementHandler::with_config(config);
302        handler.stats().increment_requests();
303
304        let cloned = handler.clone();
305        assert_eq!(cloned.stats().get_stats().total_requests, 1);
306    }
307
308    #[test]
309    fn test_stats_collector_from_handler() {
310        let config = ManagementConfig {
311            enabled: true,
312            health_path: "/health".to_string(),
313            ready_path: "/ready".to_string(),
314            stats_path: "/stats".to_string(),
315        };
316        let handler = ManagementHandler::with_config(config);
317        handler.stats().increment_requests();
318        handler.stats().add_bytes_sent(1000);
319
320        let collector = handler.stats_collector();
321        let stats = collector.get_stats();
322        assert_eq!(stats.total_requests, 1);
323        assert_eq!(stats.bytes_sent, 1000);
324    }
325
326    #[test]
327    fn test_management_response_serialization() {
328        let response = ManagementResponse {
329            status: "healthy".to_string(),
330        };
331        let json = serde_json::to_string(&response).unwrap();
332        assert_eq!(json, r#"{"status":"healthy"}"#);
333    }
334
335    #[test]
336    fn test_management_response_deserialization() {
337        let json = r#"{"status":"ready"}"#;
338        let response: ManagementResponse = serde_json::from_str(json).unwrap();
339        assert_eq!(response.status, "ready");
340    }
341
342    #[test]
343    fn test_management_response_equality() {
344        let response1 = ManagementResponse {
345            status: "healthy".to_string(),
346        };
347        let response2 = ManagementResponse {
348            status: "healthy".to_string(),
349        };
350        assert_eq!(response1, response2);
351    }
352
353    #[test]
354    fn test_new_with_config_and_stats() {
355        let config = ManagementConfig {
356            enabled: true,
357            health_path: "/health".to_string(),
358            ready_path: "/ready".to_string(),
359            stats_path: "/stats".to_string(),
360        };
361        let stats = StatsCollector::new();
362        stats.increment_requests();
363        stats.increment_requests();
364
365        let handler = ManagementHandler::new(config, stats);
366        assert_eq!(handler.stats().get_stats().total_requests, 2);
367    }
368
369    #[test]
370    fn test_stats_json_format() {
371        let config = ManagementConfig {
372            enabled: true,
373            health_path: "/health".to_string(),
374            ready_path: "/ready".to_string(),
375            stats_path: "/stats".to_string(),
376        };
377        let handler = ManagementHandler::with_config(config);
378        handler.stats().increment_requests();
379        handler.stats().record_cache_hit();
380        handler.stats().record_cache_miss();
381
382        let response = handler.handle_stats();
383        let body = response.into_body();
384        let bytes = futures::executor::block_on(body.collect()).unwrap().to_bytes();
385        let body_str = String::from_utf8(bytes.to_vec()).unwrap();
386
387        // Verify JSON structure
388        assert!(body_str.contains("\"active_connections\":0"));
389        assert!(body_str.contains("\"total_requests\":1"));
390        assert!(body_str.contains("\"cache_hit_rate\":0.5"));
391    }
392
393    #[test]
394    fn test_health_response_content_type() {
395        let config = ManagementConfig {
396            enabled: true,
397            health_path: "/health".to_string(),
398            ready_path: "/ready".to_string(),
399            stats_path: "/stats".to_string(),
400        };
401        let handler = ManagementHandler::with_config(config);
402
403        let response = handler.handle_health();
404        let content_type = response.headers().get("Content-Type").unwrap();
405        assert_eq!(content_type, "application/json");
406    }
407
408    #[test]
409    fn test_ready_response_content_type() {
410        let config = ManagementConfig {
411            enabled: true,
412            health_path: "/health".to_string(),
413            ready_path: "/ready".to_string(),
414            stats_path: "/stats".to_string(),
415        };
416        let handler = ManagementHandler::with_config(config);
417
418        let response = handler.handle_ready();
419        let content_type = response.headers().get("Content-Type").unwrap();
420        assert_eq!(content_type, "application/json");
421    }
422
423    #[test]
424    fn test_stats_response_content_type() {
425        let config = ManagementConfig {
426            enabled: true,
427            health_path: "/health".to_string(),
428            ready_path: "/ready".to_string(),
429            stats_path: "/stats".to_string(),
430        };
431        let handler = ManagementHandler::with_config(config);
432
433        let response = handler.handle_stats();
434        let content_type = response.headers().get("Content-Type").unwrap();
435        assert_eq!(content_type, "application/json");
436    }
437
438    #[test]
439    fn test_handler_debug() {
440        let config = ManagementConfig {
441            enabled: true,
442            health_path: "/health".to_string(),
443            ready_path: "/ready".to_string(),
444            stats_path: "/stats".to_string(),
445        };
446        let handler = ManagementHandler::with_config(config);
447        let debug_str = format!("{:?}", handler);
448        assert!(debug_str.contains("ManagementHandler"));
449    }
450
451    #[test]
452    fn test_multiple_requests_increment_counter() {
453        let config = ManagementConfig {
454            enabled: true,
455            health_path: "/health".to_string(),
456            ready_path: "/ready".to_string(),
457            stats_path: "/stats".to_string(),
458        };
459        let handler = ManagementHandler::with_config(config);
460
461        // Simulate multiple requests by incrementing stats
462        for _ in 0..10 {
463            handler.stats().increment_requests();
464        }
465
466        let response = handler.handle_stats();
467        let body = response.into_body();
468        let bytes = futures::executor::block_on(body.collect()).unwrap().to_bytes();
469        let body_str = String::from_utf8(bytes.to_vec()).unwrap();
470        assert!(body_str.contains("\"total_requests\":10"));
471    }
472
473    #[test]
474    fn test_health_not_ready_state() {
475        let config = ManagementConfig {
476            enabled: true,
477            health_path: "/health".to_string(),
478            ready_path: "/ready".to_string(),
479            stats_path: "/stats".to_string(),
480        };
481        let handler = ManagementHandler::with_config(config);
482
483        // Health check should still return 200 even if server is not ready
484        handler.stats().set_ready(false);
485        let response = handler.handle_health();
486        assert_eq!(response.status(), 200);
487    }
488}