1use bytes::Bytes;
8use http::{Response, StatusCode};
9use http_body_util::Full;
10use serde::Serialize;
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tracing::{debug, info, trace};
15
16use zentinel_config::{BuiltinHandler, Config};
17
18use crate::cache::{CacheManager, HttpCacheStats};
19
20pub struct BuiltinHandlerState {
22 start_time: Instant,
24 version: String,
26 instance_id: String,
28}
29
30impl BuiltinHandlerState {
31 pub fn new(version: String, instance_id: String) -> Self {
33 Self {
34 start_time: Instant::now(),
35 version,
36 instance_id,
37 }
38 }
39
40 pub fn uptime(&self) -> Duration {
42 self.start_time.elapsed()
43 }
44
45 pub fn uptime_string(&self) -> String {
47 let uptime = self.uptime();
48 let secs = uptime.as_secs();
49 let days = secs / 86400;
50 let hours = (secs % 86400) / 3600;
51 let mins = (secs % 3600) / 60;
52 let secs = secs % 60;
53
54 if days > 0 {
55 format!("{}d {}h {}m {}s", days, hours, mins, secs)
56 } else if hours > 0 {
57 format!("{}h {}m {}s", hours, mins, secs)
58 } else if mins > 0 {
59 format!("{}m {}s", mins, secs)
60 } else {
61 format!("{}s", secs)
62 }
63 }
64}
65
66#[derive(Debug, Serialize)]
68pub struct StatusResponse {
69 pub status: &'static str,
71 pub version: String,
73 pub uptime: String,
75 pub uptime_secs: u64,
77 pub instance_id: String,
79 pub timestamp: String,
81}
82
83#[derive(Debug, Serialize)]
85pub struct HealthResponse {
86 pub status: &'static str,
88 pub timestamp: String,
90}
91
92#[derive(Debug, Clone, Default)]
94pub struct UpstreamHealthSnapshot {
95 pub upstreams: HashMap<String, UpstreamStatus>,
97}
98
99#[derive(Debug, Clone, Serialize)]
101pub struct UpstreamStatus {
102 pub id: String,
104 pub load_balancing: String,
106 pub targets: Vec<TargetStatus>,
108}
109
110#[derive(Debug, Clone, Serialize)]
112pub struct TargetStatus {
113 pub address: String,
115 pub weight: u32,
117 pub status: TargetHealthStatus,
119 pub failure_rate: Option<f64>,
121 pub last_error: Option<String>,
123}
124
125#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
127#[serde(rename_all = "lowercase")]
128pub enum TargetHealthStatus {
129 Healthy,
131 Unhealthy,
133 Unknown,
135}
136
137#[derive(Debug, Clone)]
139pub struct CachePurgeRequest {
140 pub pattern: String,
142 pub wildcard: bool,
144}
145
146pub fn execute_handler(
148 handler: BuiltinHandler,
149 state: &BuiltinHandlerState,
150 request_id: &str,
151 config: Option<Arc<Config>>,
152 upstreams: Option<UpstreamHealthSnapshot>,
153 cache_stats: Option<Arc<HttpCacheStats>>,
154 cache_purge: Option<CachePurgeRequest>,
155 cache_manager: Option<&Arc<CacheManager>>,
156) -> Response<Full<Bytes>> {
157 trace!(
158 handler = ?handler,
159 request_id = %request_id,
160 "Executing builtin handler"
161 );
162
163 let response = match handler {
164 BuiltinHandler::Status => status_handler(state, request_id),
165 BuiltinHandler::Health => health_handler(request_id),
166 BuiltinHandler::Metrics => metrics_handler(request_id, cache_stats.as_ref()),
167 BuiltinHandler::NotFound => not_found_handler(request_id),
168 BuiltinHandler::Config => config_handler(config, request_id),
169 BuiltinHandler::Upstreams => upstreams_handler(upstreams, request_id),
170 BuiltinHandler::CachePurge => cache_purge_handler(cache_purge, cache_manager, request_id),
171 BuiltinHandler::CacheStats => cache_stats_handler(cache_stats, request_id),
172 };
173
174 debug!(
175 handler = ?handler,
176 request_id = %request_id,
177 status = response.status().as_u16(),
178 "Builtin handler completed"
179 );
180
181 response
182}
183
184fn status_handler(state: &BuiltinHandlerState, request_id: &str) -> Response<Full<Bytes>> {
186 trace!(
187 request_id = %request_id,
188 uptime_secs = state.uptime().as_secs(),
189 "Generating status response"
190 );
191
192 let response = StatusResponse {
193 status: "ok",
194 version: state.version.clone(),
195 uptime: state.uptime_string(),
196 uptime_secs: state.uptime().as_secs(),
197 instance_id: state.instance_id.clone(),
198 timestamp: chrono::Utc::now().to_rfc3339(),
199 };
200
201 let body =
202 serde_json::to_vec_pretty(&response).unwrap_or_else(|_| b"{\"status\":\"ok\"}".to_vec());
203
204 Response::builder()
205 .status(StatusCode::OK)
206 .header("Content-Type", "application/json; charset=utf-8")
207 .header("X-Request-Id", request_id)
208 .header("Cache-Control", "no-cache, no-store, must-revalidate")
209 .body(Full::new(Bytes::from(body)))
210 .expect("static response builder with valid headers cannot fail")
211}
212
213fn health_handler(request_id: &str) -> Response<Full<Bytes>> {
215 let response = HealthResponse {
216 status: "healthy",
217 timestamp: chrono::Utc::now().to_rfc3339(),
218 };
219
220 let body =
221 serde_json::to_vec(&response).unwrap_or_else(|_| b"{\"status\":\"healthy\"}".to_vec());
222
223 Response::builder()
224 .status(StatusCode::OK)
225 .header("Content-Type", "application/json; charset=utf-8")
226 .header("X-Request-Id", request_id)
227 .header("Cache-Control", "no-cache, no-store, must-revalidate")
228 .body(Full::new(Bytes::from(body)))
229 .expect("static response builder with valid headers cannot fail")
230}
231
232pub(crate) const PROMETHEUS_CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8";
234
235pub(crate) fn render_prometheus_metrics(
245 cache_stats: Option<&Arc<HttpCacheStats>>,
246) -> Result<Vec<u8>, prometheus::Error> {
247 use prometheus::{Encoder, TextEncoder};
248
249 let encoder = TextEncoder::new();
250 let metric_families = prometheus::gather();
251
252 let mut buffer = Vec::new();
253 encoder.encode(&metric_families, &mut buffer)?;
254
255 let extra_metrics = format!(
257 "# HELP zentinel_up Zentinel proxy is up and running\n\
258 # TYPE zentinel_up gauge\n\
259 zentinel_up 1\n\
260 # HELP zentinel_build_info Build information\n\
261 # TYPE zentinel_build_info gauge\n\
262 zentinel_build_info{{version=\"{}\"}} 1\n",
263 env!("CARGO_PKG_VERSION")
264 );
265 buffer.extend_from_slice(extra_metrics.as_bytes());
266
267 if let Some(stats) = cache_stats {
269 let cache_metrics = format!(
270 "# HELP zentinel_cache_hits_total Total number of cache hits\n\
271 # TYPE zentinel_cache_hits_total counter\n\
272 zentinel_cache_hits_total {}\n\
273 # HELP zentinel_cache_misses_total Total number of cache misses\n\
274 # TYPE zentinel_cache_misses_total counter\n\
275 zentinel_cache_misses_total {}\n\
276 # HELP zentinel_cache_stores_total Total number of cache stores\n\
277 # TYPE zentinel_cache_stores_total counter\n\
278 zentinel_cache_stores_total {}\n\
279 # HELP zentinel_cache_hit_ratio Cache hit ratio (0.0 to 1.0)\n\
280 # TYPE zentinel_cache_hit_ratio gauge\n\
281 zentinel_cache_hit_ratio {:.4}\n\
282 # HELP zentinel_cache_memory_hits_total Cache hits from memory tier\n\
283 # TYPE zentinel_cache_memory_hits_total counter\n\
284 zentinel_cache_memory_hits_total {}\n\
285 # HELP zentinel_cache_disk_hits_total Cache hits from disk tier\n\
286 # TYPE zentinel_cache_disk_hits_total counter\n\
287 zentinel_cache_disk_hits_total {}\n",
288 stats.hits(),
289 stats.misses(),
290 stats.stores(),
291 stats.hit_ratio(),
292 stats.memory_hits(),
293 stats.disk_hits()
294 );
295 buffer.extend_from_slice(cache_metrics.as_bytes());
296 }
297
298 Ok(buffer)
299}
300
301fn metrics_handler(
303 request_id: &str,
304 cache_stats: Option<&Arc<HttpCacheStats>>,
305) -> Response<Full<Bytes>> {
306 match render_prometheus_metrics(cache_stats) {
307 Ok(buffer) => Response::builder()
308 .status(StatusCode::OK)
309 .header("Content-Type", PROMETHEUS_CONTENT_TYPE)
310 .header("X-Request-Id", request_id)
311 .body(Full::new(Bytes::from(buffer)))
312 .expect("static response builder with valid headers cannot fail"),
313 Err(e) => {
314 tracing::error!(error = %e, "Failed to encode Prometheus metrics");
315 let error_body = format!("# ERROR: Failed to encode metrics: {}\n", e);
316 Response::builder()
317 .status(StatusCode::INTERNAL_SERVER_ERROR)
318 .header("Content-Type", "text/plain; charset=utf-8")
319 .header("X-Request-Id", request_id)
320 .body(Full::new(Bytes::from(error_body)))
321 .expect("static response builder with valid headers cannot fail")
322 }
323 }
324}
325
326fn not_found_handler(request_id: &str) -> Response<Full<Bytes>> {
328 let body = serde_json::json!({
329 "error": "Not Found",
330 "status": 404,
331 "message": "The requested resource could not be found.",
332 "request_id": request_id,
333 "timestamp": chrono::Utc::now().to_rfc3339(),
334 });
335
336 let body_bytes = serde_json::to_vec_pretty(&body)
337 .unwrap_or_else(|_| b"{\"error\":\"Not Found\",\"status\":404}".to_vec());
338
339 Response::builder()
340 .status(StatusCode::NOT_FOUND)
341 .header("Content-Type", "application/json; charset=utf-8")
342 .header("X-Request-Id", request_id)
343 .body(Full::new(Bytes::from(body_bytes)))
344 .expect("static response builder with valid headers cannot fail")
345}
346
347fn config_handler(config: Option<Arc<Config>>, request_id: &str) -> Response<Full<Bytes>> {
352 let body = match &config {
353 Some(cfg) => {
354 let response = serde_json::json!({
358 "timestamp": chrono::Utc::now().to_rfc3339(),
359 "request_id": request_id,
360 "config": {
361 "server": &cfg.server,
362 "listeners": cfg.listeners.iter().map(|l| {
363 serde_json::json!({
364 "id": l.id,
365 "address": l.address,
366 "protocol": l.protocol,
367 "default_route": l.default_route,
368 "request_timeout_secs": l.request_timeout_secs,
369 "keepalive_timeout_secs": l.keepalive_timeout_secs,
370 "tls_enabled": l.tls.is_some(),
372 })
373 }).collect::<Vec<_>>(),
374 "routes": cfg.routes.iter().map(|r| {
375 serde_json::json!({
376 "id": r.id,
377 "priority": r.priority,
378 "matches": r.matches,
379 "upstream": r.upstream,
380 "service_type": r.service_type,
381 "builtin_handler": r.builtin_handler,
382 "filters": r.filters,
383 "waf_enabled": r.waf_enabled,
384 })
385 }).collect::<Vec<_>>(),
386 "upstreams": cfg.upstreams.iter().map(|(id, u)| {
387 serde_json::json!({
388 "id": id,
389 "targets": u.targets.iter().map(|t| {
390 serde_json::json!({
391 "address": t.address,
392 "weight": t.weight,
393 })
394 }).collect::<Vec<_>>(),
395 "load_balancing": u.load_balancing,
396 "health_check": u.health_check.as_ref().map(|h| {
397 serde_json::json!({
398 "interval_secs": h.interval_secs,
399 "timeout_secs": h.timeout_secs,
400 "healthy_threshold": h.healthy_threshold,
401 "unhealthy_threshold": h.unhealthy_threshold,
402 })
403 }),
404 "tls_enabled": u.tls.is_some(),
406 })
407 }).collect::<Vec<_>>(),
408 "agents": cfg.agents.iter().map(|a| {
409 serde_json::json!({
410 "id": a.id,
411 "agent_type": a.agent_type,
412 "timeout_ms": a.timeout_ms,
413 })
414 }).collect::<Vec<_>>(),
415 "filters": cfg.filters.keys().collect::<Vec<_>>(),
416 "waf": cfg.waf.as_ref().map(|w| {
417 serde_json::json!({
418 "mode": w.mode,
419 "engine": w.engine,
420 "audit_log": w.audit_log,
421 })
422 }),
423 "limits": &cfg.limits,
424 }
425 });
426
427 serde_json::to_vec_pretty(&response).unwrap_or_else(|e| {
428 serde_json::to_vec(&serde_json::json!({
429 "error": "Failed to serialize config",
430 "message": e.to_string(),
431 }))
432 .unwrap_or_default()
433 })
434 }
435 None => serde_json::to_vec_pretty(&serde_json::json!({
436 "error": "Configuration unavailable",
437 "status": 503,
438 "message": "Config manager not available",
439 "request_id": request_id,
440 "timestamp": chrono::Utc::now().to_rfc3339(),
441 }))
442 .unwrap_or_default(),
443 };
444
445 let status = if config.is_some() {
446 StatusCode::OK
447 } else {
448 StatusCode::SERVICE_UNAVAILABLE
449 };
450
451 Response::builder()
452 .status(status)
453 .header("Content-Type", "application/json; charset=utf-8")
454 .header("X-Request-Id", request_id)
455 .header("Cache-Control", "no-cache, no-store, must-revalidate")
456 .body(Full::new(Bytes::from(body)))
457 .expect("static response builder with valid headers cannot fail")
458}
459
460fn upstreams_handler(
464 snapshot: Option<UpstreamHealthSnapshot>,
465 request_id: &str,
466) -> Response<Full<Bytes>> {
467 let body = match snapshot {
468 Some(data) => {
469 let mut total_healthy = 0;
471 let mut total_unhealthy = 0;
472 let mut total_unknown = 0;
473
474 for upstream in data.upstreams.values() {
475 for target in &upstream.targets {
476 match target.status {
477 TargetHealthStatus::Healthy => total_healthy += 1,
478 TargetHealthStatus::Unhealthy => total_unhealthy += 1,
479 TargetHealthStatus::Unknown => total_unknown += 1,
480 }
481 }
482 }
483
484 let response = serde_json::json!({
485 "timestamp": chrono::Utc::now().to_rfc3339(),
486 "request_id": request_id,
487 "summary": {
488 "total_upstreams": data.upstreams.len(),
489 "total_targets": total_healthy + total_unhealthy + total_unknown,
490 "healthy": total_healthy,
491 "unhealthy": total_unhealthy,
492 "unknown": total_unknown,
493 },
494 "upstreams": data.upstreams.values().collect::<Vec<_>>(),
495 });
496
497 serde_json::to_vec_pretty(&response).unwrap_or_else(|e| {
498 serde_json::to_vec(&serde_json::json!({
499 "error": "Failed to serialize upstreams",
500 "message": e.to_string(),
501 }))
502 .unwrap_or_default()
503 })
504 }
505 None => {
506 serde_json::to_vec_pretty(&serde_json::json!({
508 "timestamp": chrono::Utc::now().to_rfc3339(),
509 "request_id": request_id,
510 "summary": {
511 "total_upstreams": 0,
512 "total_targets": 0,
513 "healthy": 0,
514 "unhealthy": 0,
515 "unknown": 0,
516 },
517 "upstreams": [],
518 "message": "No upstreams configured",
519 }))
520 .unwrap_or_default()
521 }
522 };
523
524 Response::builder()
525 .status(StatusCode::OK)
526 .header("Content-Type", "application/json; charset=utf-8")
527 .header("X-Request-Id", request_id)
528 .header("Cache-Control", "no-cache, no-store, must-revalidate")
529 .body(Full::new(Bytes::from(body)))
530 .expect("static response builder with valid headers cannot fail")
531}
532
533fn cache_purge_handler(
538 purge_request: Option<CachePurgeRequest>,
539 cache_manager: Option<&Arc<CacheManager>>,
540 request_id: &str,
541) -> Response<Full<Bytes>> {
542 let body = match (&purge_request, cache_manager) {
543 (Some(request), Some(manager)) => {
544 info!(
545 pattern = %request.pattern,
546 wildcard = request.wildcard,
547 request_id = %request_id,
548 "Processing cache purge request"
549 );
550
551 let purged_count = if request.wildcard {
553 manager.purge_wildcard(&request.pattern)
555 } else {
556 manager.purge(&request.pattern)
558 };
559
560 info!(
561 pattern = %request.pattern,
562 wildcard = request.wildcard,
563 purged_count = purged_count,
564 request_id = %request_id,
565 "Cache purge completed"
566 );
567
568 serde_json::to_vec_pretty(&serde_json::json!({
569 "status": "ok",
570 "message": "Cache purge request processed",
571 "pattern": request.pattern,
572 "wildcard": request.wildcard,
573 "purged_entries": purged_count,
574 "active_purges": manager.active_purge_count(),
575 "request_id": request_id,
576 "timestamp": chrono::Utc::now().to_rfc3339(),
577 }))
578 .unwrap_or_default()
579 }
580 (Some(request), None) => {
581 tracing::warn!(
583 pattern = %request.pattern,
584 request_id = %request_id,
585 "Cache purge requested but cache manager not available"
586 );
587
588 serde_json::to_vec_pretty(&serde_json::json!({
589 "status": "warning",
590 "message": "Cache purge acknowledged but cache manager unavailable",
591 "pattern": request.pattern,
592 "wildcard": request.wildcard,
593 "purged_entries": 0,
594 "request_id": request_id,
595 "timestamp": chrono::Utc::now().to_rfc3339(),
596 }))
597 .unwrap_or_default()
598 }
599 (None, _) => {
600 serde_json::to_vec_pretty(&serde_json::json!({
602 "error": "Bad Request",
603 "status": 400,
604 "message": "Cache purge requires a pattern. Use PURGE /path or X-Purge-Pattern header.",
605 "request_id": request_id,
606 "timestamp": chrono::Utc::now().to_rfc3339(),
607 })).unwrap_or_default()
608 }
609 };
610
611 let status = if purge_request.is_some() {
612 StatusCode::OK
613 } else {
614 StatusCode::BAD_REQUEST
615 };
616
617 Response::builder()
618 .status(status)
619 .header("Content-Type", "application/json; charset=utf-8")
620 .header("X-Request-Id", request_id)
621 .header("Cache-Control", "no-cache, no-store, must-revalidate")
622 .body(Full::new(Bytes::from(body)))
623 .expect("static response builder with valid headers cannot fail")
624}
625
626#[derive(Debug, Serialize)]
628struct CacheStatsResponse {
629 hits: u64,
631 misses: u64,
633 stores: u64,
635 evictions: u64,
637 hit_ratio: f64,
639 memory_hits: u64,
641 disk_hits: u64,
643 request_id: String,
645 timestamp: String,
647}
648
649fn cache_stats_handler(
653 cache_stats: Option<Arc<HttpCacheStats>>,
654 request_id: &str,
655) -> Response<Full<Bytes>> {
656 let body = match cache_stats {
657 Some(stats) => {
658 let response = CacheStatsResponse {
659 hits: stats.hits(),
660 misses: stats.misses(),
661 stores: stats.stores(),
662 evictions: stats.evictions(),
663 hit_ratio: stats.hit_ratio(),
664 memory_hits: stats.memory_hits(),
665 disk_hits: stats.disk_hits(),
666 request_id: request_id.to_string(),
667 timestamp: chrono::Utc::now().to_rfc3339(),
668 };
669
670 serde_json::to_vec_pretty(&response)
671 .unwrap_or_else(|_| b"{\"error\":\"Failed to serialize stats\"}".to_vec())
672 }
673 None => serde_json::to_vec_pretty(&serde_json::json!({
674 "hits": 0,
675 "misses": 0,
676 "stores": 0,
677 "evictions": 0,
678 "hit_ratio": 0.0,
679 "memory_hits": 0,
680 "disk_hits": 0,
681 "message": "Cache statistics not available",
682 "request_id": request_id,
683 "timestamp": chrono::Utc::now().to_rfc3339(),
684 }))
685 .unwrap_or_default(),
686 };
687
688 Response::builder()
689 .status(StatusCode::OK)
690 .header("Content-Type", "application/json; charset=utf-8")
691 .header("X-Request-Id", request_id)
692 .header("Cache-Control", "no-cache, no-store, must-revalidate")
693 .body(Full::new(Bytes::from(body)))
694 .expect("static response builder with valid headers cannot fail")
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn test_status_handler() {
703 let state = BuiltinHandlerState::new("0.1.0".to_string(), "test-instance".to_string());
704
705 let response = status_handler(&state, "test-request-id");
706 assert_eq!(response.status(), StatusCode::OK);
707
708 let content_type = response.headers().get("Content-Type").unwrap();
709 assert_eq!(content_type, "application/json; charset=utf-8");
710 }
711
712 #[test]
713 fn test_health_handler() {
714 let response = health_handler("test-request-id");
715 assert_eq!(response.status(), StatusCode::OK);
716 }
717
718 #[test]
719 fn test_metrics_handler() {
720 let response = metrics_handler("test-request-id", None);
721 assert_eq!(response.status(), StatusCode::OK);
722
723 let content_type = response.headers().get("Content-Type").unwrap();
724 assert!(content_type.to_str().unwrap().contains("text/plain"));
725 }
726
727 #[test]
728 fn test_metrics_handler_with_cache_stats() {
729 let stats = Arc::new(HttpCacheStats::default());
730 stats.record_hit();
731 stats.record_miss();
732 stats.record_store();
733
734 let response = metrics_handler("test-request-id", Some(&stats));
735 assert_eq!(response.status(), StatusCode::OK);
736 }
737
738 #[test]
739 fn test_cache_purge_handler_with_request() {
740 let cache_manager = Arc::new(CacheManager::new());
741 let request = CachePurgeRequest {
742 pattern: "/api/users/*".to_string(),
743 wildcard: true,
744 };
745 let response = cache_purge_handler(Some(request), Some(&cache_manager), "test-request-id");
746 assert_eq!(response.status(), StatusCode::OK);
747
748 assert!(cache_manager.active_purge_count() > 0);
750 }
751
752 #[test]
753 fn test_cache_purge_handler_single_entry() {
754 let cache_manager = Arc::new(CacheManager::new());
755 let request = CachePurgeRequest {
756 pattern: "/api/users/123".to_string(),
757 wildcard: false,
758 };
759 let response = cache_purge_handler(Some(request), Some(&cache_manager), "test-request-id");
760 assert_eq!(response.status(), StatusCode::OK);
761
762 assert!(cache_manager.should_invalidate("/api/users/123"));
764 }
765
766 #[test]
767 fn test_cache_purge_handler_without_request() {
768 let cache_manager = Arc::new(CacheManager::new());
769 let response = cache_purge_handler(None, Some(&cache_manager), "test-request-id");
770 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
771 }
772
773 #[test]
774 fn test_cache_purge_handler_without_manager() {
775 let request = CachePurgeRequest {
776 pattern: "/api/users/*".to_string(),
777 wildcard: true,
778 };
779 let response = cache_purge_handler(Some(request), None, "test-request-id");
781 assert_eq!(response.status(), StatusCode::OK);
782 }
783
784 #[test]
785 fn test_cache_stats_handler_with_stats() {
786 let stats = Arc::new(HttpCacheStats::default());
787 stats.record_hit();
788 stats.record_hit();
789 stats.record_miss();
790
791 let response = cache_stats_handler(Some(stats), "test-request-id");
792 assert_eq!(response.status(), StatusCode::OK);
793
794 let content_type = response.headers().get("Content-Type").unwrap();
795 assert_eq!(content_type, "application/json; charset=utf-8");
796 }
797
798 #[test]
799 fn test_cache_stats_handler_without_stats() {
800 let response = cache_stats_handler(None, "test-request-id");
801 assert_eq!(response.status(), StatusCode::OK);
802 }
803
804 #[test]
805 fn test_not_found_handler() {
806 let response = not_found_handler("test-request-id");
807 assert_eq!(response.status(), StatusCode::NOT_FOUND);
808 }
809
810 #[test]
811 fn test_config_handler_with_config() {
812 let config = Arc::new(Config::default_for_testing());
813 let response = config_handler(Some(config), "test-request-id");
814 assert_eq!(response.status(), StatusCode::OK);
815
816 let content_type = response.headers().get("Content-Type").unwrap();
817 assert_eq!(content_type, "application/json; charset=utf-8");
818 }
819
820 #[test]
821 fn test_config_handler_without_config() {
822 let response = config_handler(None, "test-request-id");
823 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
824 }
825
826 #[test]
827 fn test_upstreams_handler_with_data() {
828 let mut upstreams = HashMap::new();
829 upstreams.insert(
830 "backend".to_string(),
831 UpstreamStatus {
832 id: "backend".to_string(),
833 load_balancing: "round_robin".to_string(),
834 targets: vec![
835 TargetStatus {
836 address: "10.0.0.1:8080".to_string(),
837 weight: 1,
838 status: TargetHealthStatus::Healthy,
839 failure_rate: Some(0.0),
840 last_error: None,
841 },
842 TargetStatus {
843 address: "10.0.0.2:8080".to_string(),
844 weight: 1,
845 status: TargetHealthStatus::Unhealthy,
846 failure_rate: Some(0.8),
847 last_error: Some("connection refused".to_string()),
848 },
849 ],
850 },
851 );
852
853 let snapshot = UpstreamHealthSnapshot { upstreams };
854 let response = upstreams_handler(Some(snapshot), "test-request-id");
855 assert_eq!(response.status(), StatusCode::OK);
856
857 let content_type = response.headers().get("Content-Type").unwrap();
858 assert_eq!(content_type, "application/json; charset=utf-8");
859 }
860
861 #[test]
862 fn test_upstreams_handler_no_upstreams() {
863 let response = upstreams_handler(None, "test-request-id");
864 assert_eq!(response.status(), StatusCode::OK);
865 }
866
867 #[test]
868 fn test_uptime_formatting() {
869 let state = BuiltinHandlerState::new("0.1.0".to_string(), "test".to_string());
870
871 let uptime = state.uptime_string();
873 assert!(!uptime.is_empty());
874 }
875}