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 grapsus_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
232fn metrics_handler(
234 request_id: &str,
235 cache_stats: Option<&Arc<HttpCacheStats>>,
236) -> Response<Full<Bytes>> {
237 use prometheus::{Encoder, TextEncoder};
238
239 let encoder = TextEncoder::new();
241
242 let metric_families = prometheus::gather();
244
245 let mut buffer = Vec::new();
247 match encoder.encode(&metric_families, &mut buffer) {
248 Ok(()) => {
249 let extra_metrics = format!(
251 "# HELP grapsus_up Grapsus proxy is up and running\n\
252 # TYPE grapsus_up gauge\n\
253 grapsus_up 1\n\
254 # HELP grapsus_build_info Build information\n\
255 # TYPE grapsus_build_info gauge\n\
256 grapsus_build_info{{version=\"{}\"}} 1\n",
257 env!("CARGO_PKG_VERSION")
258 );
259 buffer.extend_from_slice(extra_metrics.as_bytes());
260
261 if let Some(stats) = cache_stats {
263 let cache_metrics = format!(
264 "# HELP grapsus_cache_hits_total Total number of cache hits\n\
265 # TYPE grapsus_cache_hits_total counter\n\
266 grapsus_cache_hits_total {}\n\
267 # HELP grapsus_cache_misses_total Total number of cache misses\n\
268 # TYPE grapsus_cache_misses_total counter\n\
269 grapsus_cache_misses_total {}\n\
270 # HELP grapsus_cache_stores_total Total number of cache stores\n\
271 # TYPE grapsus_cache_stores_total counter\n\
272 grapsus_cache_stores_total {}\n\
273 # HELP grapsus_cache_hit_ratio Cache hit ratio (0.0 to 1.0)\n\
274 # TYPE grapsus_cache_hit_ratio gauge\n\
275 grapsus_cache_hit_ratio {:.4}\n\
276 # HELP grapsus_cache_memory_hits_total Cache hits from memory tier\n\
277 # TYPE grapsus_cache_memory_hits_total counter\n\
278 grapsus_cache_memory_hits_total {}\n\
279 # HELP grapsus_cache_disk_hits_total Cache hits from disk tier\n\
280 # TYPE grapsus_cache_disk_hits_total counter\n\
281 grapsus_cache_disk_hits_total {}\n",
282 stats.hits(),
283 stats.misses(),
284 stats.stores(),
285 stats.hit_ratio(),
286 stats.memory_hits(),
287 stats.disk_hits()
288 );
289 buffer.extend_from_slice(cache_metrics.as_bytes());
290 }
291
292 Response::builder()
293 .status(StatusCode::OK)
294 .header("Content-Type", encoder.format_type())
295 .header("X-Request-Id", request_id)
296 .body(Full::new(Bytes::from(buffer)))
297 .expect("static response builder with valid headers cannot fail")
298 }
299 Err(e) => {
300 tracing::error!(error = %e, "Failed to encode Prometheus metrics");
301 let error_body = format!("# ERROR: Failed to encode metrics: {}\n", e);
302 Response::builder()
303 .status(StatusCode::INTERNAL_SERVER_ERROR)
304 .header("Content-Type", "text/plain; charset=utf-8")
305 .header("X-Request-Id", request_id)
306 .body(Full::new(Bytes::from(error_body)))
307 .expect("static response builder with valid headers cannot fail")
308 }
309 }
310}
311
312fn not_found_handler(request_id: &str) -> Response<Full<Bytes>> {
314 let body = serde_json::json!({
315 "error": "Not Found",
316 "status": 404,
317 "message": "The requested resource could not be found.",
318 "request_id": request_id,
319 "timestamp": chrono::Utc::now().to_rfc3339(),
320 });
321
322 let body_bytes = serde_json::to_vec_pretty(&body)
323 .unwrap_or_else(|_| b"{\"error\":\"Not Found\",\"status\":404}".to_vec());
324
325 Response::builder()
326 .status(StatusCode::NOT_FOUND)
327 .header("Content-Type", "application/json; charset=utf-8")
328 .header("X-Request-Id", request_id)
329 .body(Full::new(Bytes::from(body_bytes)))
330 .expect("static response builder with valid headers cannot fail")
331}
332
333fn config_handler(config: Option<Arc<Config>>, request_id: &str) -> Response<Full<Bytes>> {
338 let body = match &config {
339 Some(cfg) => {
340 let response = serde_json::json!({
344 "timestamp": chrono::Utc::now().to_rfc3339(),
345 "request_id": request_id,
346 "config": {
347 "server": &cfg.server,
348 "listeners": cfg.listeners.iter().map(|l| {
349 serde_json::json!({
350 "id": l.id,
351 "address": l.address,
352 "protocol": l.protocol,
353 "default_route": l.default_route,
354 "request_timeout_secs": l.request_timeout_secs,
355 "keepalive_timeout_secs": l.keepalive_timeout_secs,
356 "tls_enabled": l.tls.is_some(),
358 })
359 }).collect::<Vec<_>>(),
360 "routes": cfg.routes.iter().map(|r| {
361 serde_json::json!({
362 "id": r.id,
363 "priority": r.priority,
364 "matches": r.matches,
365 "upstream": r.upstream,
366 "service_type": r.service_type,
367 "builtin_handler": r.builtin_handler,
368 "filters": r.filters,
369 "waf_enabled": r.waf_enabled,
370 })
371 }).collect::<Vec<_>>(),
372 "upstreams": cfg.upstreams.iter().map(|(id, u)| {
373 serde_json::json!({
374 "id": id,
375 "targets": u.targets.iter().map(|t| {
376 serde_json::json!({
377 "address": t.address,
378 "weight": t.weight,
379 })
380 }).collect::<Vec<_>>(),
381 "load_balancing": u.load_balancing,
382 "health_check": u.health_check.as_ref().map(|h| {
383 serde_json::json!({
384 "interval_secs": h.interval_secs,
385 "timeout_secs": h.timeout_secs,
386 "healthy_threshold": h.healthy_threshold,
387 "unhealthy_threshold": h.unhealthy_threshold,
388 })
389 }),
390 "tls_enabled": u.tls.is_some(),
392 })
393 }).collect::<Vec<_>>(),
394 "agents": cfg.agents.iter().map(|a| {
395 serde_json::json!({
396 "id": a.id,
397 "agent_type": a.agent_type,
398 "timeout_ms": a.timeout_ms,
399 })
400 }).collect::<Vec<_>>(),
401 "filters": cfg.filters.keys().collect::<Vec<_>>(),
402 "waf": cfg.waf.as_ref().map(|w| {
403 serde_json::json!({
404 "mode": w.mode,
405 "engine": w.engine,
406 "audit_log": w.audit_log,
407 })
408 }),
409 "limits": &cfg.limits,
410 }
411 });
412
413 serde_json::to_vec_pretty(&response).unwrap_or_else(|e| {
414 serde_json::to_vec(&serde_json::json!({
415 "error": "Failed to serialize config",
416 "message": e.to_string(),
417 }))
418 .unwrap_or_default()
419 })
420 }
421 None => serde_json::to_vec_pretty(&serde_json::json!({
422 "error": "Configuration unavailable",
423 "status": 503,
424 "message": "Config manager not available",
425 "request_id": request_id,
426 "timestamp": chrono::Utc::now().to_rfc3339(),
427 }))
428 .unwrap_or_default(),
429 };
430
431 let status = if config.is_some() {
432 StatusCode::OK
433 } else {
434 StatusCode::SERVICE_UNAVAILABLE
435 };
436
437 Response::builder()
438 .status(status)
439 .header("Content-Type", "application/json; charset=utf-8")
440 .header("X-Request-Id", request_id)
441 .header("Cache-Control", "no-cache, no-store, must-revalidate")
442 .body(Full::new(Bytes::from(body)))
443 .expect("static response builder with valid headers cannot fail")
444}
445
446fn upstreams_handler(
450 snapshot: Option<UpstreamHealthSnapshot>,
451 request_id: &str,
452) -> Response<Full<Bytes>> {
453 let body = match snapshot {
454 Some(data) => {
455 let mut total_healthy = 0;
457 let mut total_unhealthy = 0;
458 let mut total_unknown = 0;
459
460 for upstream in data.upstreams.values() {
461 for target in &upstream.targets {
462 match target.status {
463 TargetHealthStatus::Healthy => total_healthy += 1,
464 TargetHealthStatus::Unhealthy => total_unhealthy += 1,
465 TargetHealthStatus::Unknown => total_unknown += 1,
466 }
467 }
468 }
469
470 let response = serde_json::json!({
471 "timestamp": chrono::Utc::now().to_rfc3339(),
472 "request_id": request_id,
473 "summary": {
474 "total_upstreams": data.upstreams.len(),
475 "total_targets": total_healthy + total_unhealthy + total_unknown,
476 "healthy": total_healthy,
477 "unhealthy": total_unhealthy,
478 "unknown": total_unknown,
479 },
480 "upstreams": data.upstreams.values().collect::<Vec<_>>(),
481 });
482
483 serde_json::to_vec_pretty(&response).unwrap_or_else(|e| {
484 serde_json::to_vec(&serde_json::json!({
485 "error": "Failed to serialize upstreams",
486 "message": e.to_string(),
487 }))
488 .unwrap_or_default()
489 })
490 }
491 None => {
492 serde_json::to_vec_pretty(&serde_json::json!({
494 "timestamp": chrono::Utc::now().to_rfc3339(),
495 "request_id": request_id,
496 "summary": {
497 "total_upstreams": 0,
498 "total_targets": 0,
499 "healthy": 0,
500 "unhealthy": 0,
501 "unknown": 0,
502 },
503 "upstreams": [],
504 "message": "No upstreams configured",
505 }))
506 .unwrap_or_default()
507 }
508 };
509
510 Response::builder()
511 .status(StatusCode::OK)
512 .header("Content-Type", "application/json; charset=utf-8")
513 .header("X-Request-Id", request_id)
514 .header("Cache-Control", "no-cache, no-store, must-revalidate")
515 .body(Full::new(Bytes::from(body)))
516 .expect("static response builder with valid headers cannot fail")
517}
518
519fn cache_purge_handler(
524 purge_request: Option<CachePurgeRequest>,
525 cache_manager: Option<&Arc<CacheManager>>,
526 request_id: &str,
527) -> Response<Full<Bytes>> {
528 let body = match (&purge_request, cache_manager) {
529 (Some(request), Some(manager)) => {
530 info!(
531 pattern = %request.pattern,
532 wildcard = request.wildcard,
533 request_id = %request_id,
534 "Processing cache purge request"
535 );
536
537 let purged_count = if request.wildcard {
539 manager.purge_wildcard(&request.pattern)
541 } else {
542 manager.purge(&request.pattern)
544 };
545
546 info!(
547 pattern = %request.pattern,
548 wildcard = request.wildcard,
549 purged_count = purged_count,
550 request_id = %request_id,
551 "Cache purge completed"
552 );
553
554 serde_json::to_vec_pretty(&serde_json::json!({
555 "status": "ok",
556 "message": "Cache purge request processed",
557 "pattern": request.pattern,
558 "wildcard": request.wildcard,
559 "purged_entries": purged_count,
560 "active_purges": manager.active_purge_count(),
561 "request_id": request_id,
562 "timestamp": chrono::Utc::now().to_rfc3339(),
563 }))
564 .unwrap_or_default()
565 }
566 (Some(request), None) => {
567 tracing::warn!(
569 pattern = %request.pattern,
570 request_id = %request_id,
571 "Cache purge requested but cache manager not available"
572 );
573
574 serde_json::to_vec_pretty(&serde_json::json!({
575 "status": "warning",
576 "message": "Cache purge acknowledged but cache manager unavailable",
577 "pattern": request.pattern,
578 "wildcard": request.wildcard,
579 "purged_entries": 0,
580 "request_id": request_id,
581 "timestamp": chrono::Utc::now().to_rfc3339(),
582 }))
583 .unwrap_or_default()
584 }
585 (None, _) => {
586 serde_json::to_vec_pretty(&serde_json::json!({
588 "error": "Bad Request",
589 "status": 400,
590 "message": "Cache purge requires a pattern. Use PURGE /path or X-Purge-Pattern header.",
591 "request_id": request_id,
592 "timestamp": chrono::Utc::now().to_rfc3339(),
593 })).unwrap_or_default()
594 }
595 };
596
597 let status = if purge_request.is_some() {
598 StatusCode::OK
599 } else {
600 StatusCode::BAD_REQUEST
601 };
602
603 Response::builder()
604 .status(status)
605 .header("Content-Type", "application/json; charset=utf-8")
606 .header("X-Request-Id", request_id)
607 .header("Cache-Control", "no-cache, no-store, must-revalidate")
608 .body(Full::new(Bytes::from(body)))
609 .expect("static response builder with valid headers cannot fail")
610}
611
612#[derive(Debug, Serialize)]
614struct CacheStatsResponse {
615 hits: u64,
617 misses: u64,
619 stores: u64,
621 evictions: u64,
623 hit_ratio: f64,
625 memory_hits: u64,
627 disk_hits: u64,
629 request_id: String,
631 timestamp: String,
633}
634
635fn cache_stats_handler(
639 cache_stats: Option<Arc<HttpCacheStats>>,
640 request_id: &str,
641) -> Response<Full<Bytes>> {
642 let body = match cache_stats {
643 Some(stats) => {
644 let response = CacheStatsResponse {
645 hits: stats.hits(),
646 misses: stats.misses(),
647 stores: stats.stores(),
648 evictions: stats.evictions(),
649 hit_ratio: stats.hit_ratio(),
650 memory_hits: stats.memory_hits(),
651 disk_hits: stats.disk_hits(),
652 request_id: request_id.to_string(),
653 timestamp: chrono::Utc::now().to_rfc3339(),
654 };
655
656 serde_json::to_vec_pretty(&response)
657 .unwrap_or_else(|_| b"{\"error\":\"Failed to serialize stats\"}".to_vec())
658 }
659 None => serde_json::to_vec_pretty(&serde_json::json!({
660 "hits": 0,
661 "misses": 0,
662 "stores": 0,
663 "evictions": 0,
664 "hit_ratio": 0.0,
665 "memory_hits": 0,
666 "disk_hits": 0,
667 "message": "Cache statistics not available",
668 "request_id": request_id,
669 "timestamp": chrono::Utc::now().to_rfc3339(),
670 }))
671 .unwrap_or_default(),
672 };
673
674 Response::builder()
675 .status(StatusCode::OK)
676 .header("Content-Type", "application/json; charset=utf-8")
677 .header("X-Request-Id", request_id)
678 .header("Cache-Control", "no-cache, no-store, must-revalidate")
679 .body(Full::new(Bytes::from(body)))
680 .expect("static response builder with valid headers cannot fail")
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_status_handler() {
689 let state = BuiltinHandlerState::new("0.1.0".to_string(), "test-instance".to_string());
690
691 let response = status_handler(&state, "test-request-id");
692 assert_eq!(response.status(), StatusCode::OK);
693
694 let content_type = response.headers().get("Content-Type").unwrap();
695 assert_eq!(content_type, "application/json; charset=utf-8");
696 }
697
698 #[test]
699 fn test_health_handler() {
700 let response = health_handler("test-request-id");
701 assert_eq!(response.status(), StatusCode::OK);
702 }
703
704 #[test]
705 fn test_metrics_handler() {
706 let response = metrics_handler("test-request-id", None);
707 assert_eq!(response.status(), StatusCode::OK);
708
709 let content_type = response.headers().get("Content-Type").unwrap();
710 assert!(content_type.to_str().unwrap().contains("text/plain"));
711 }
712
713 #[test]
714 fn test_metrics_handler_with_cache_stats() {
715 let stats = Arc::new(HttpCacheStats::default());
716 stats.record_hit();
717 stats.record_miss();
718 stats.record_store();
719
720 let response = metrics_handler("test-request-id", Some(&stats));
721 assert_eq!(response.status(), StatusCode::OK);
722 }
723
724 #[test]
725 fn test_cache_purge_handler_with_request() {
726 let cache_manager = Arc::new(CacheManager::new());
727 let request = CachePurgeRequest {
728 pattern: "/api/users/*".to_string(),
729 wildcard: true,
730 };
731 let response = cache_purge_handler(Some(request), Some(&cache_manager), "test-request-id");
732 assert_eq!(response.status(), StatusCode::OK);
733
734 assert!(cache_manager.active_purge_count() > 0);
736 }
737
738 #[test]
739 fn test_cache_purge_handler_single_entry() {
740 let cache_manager = Arc::new(CacheManager::new());
741 let request = CachePurgeRequest {
742 pattern: "/api/users/123".to_string(),
743 wildcard: false,
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.should_invalidate("/api/users/123"));
750 }
751
752 #[test]
753 fn test_cache_purge_handler_without_request() {
754 let cache_manager = Arc::new(CacheManager::new());
755 let response = cache_purge_handler(None, Some(&cache_manager), "test-request-id");
756 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
757 }
758
759 #[test]
760 fn test_cache_purge_handler_without_manager() {
761 let request = CachePurgeRequest {
762 pattern: "/api/users/*".to_string(),
763 wildcard: true,
764 };
765 let response = cache_purge_handler(Some(request), None, "test-request-id");
767 assert_eq!(response.status(), StatusCode::OK);
768 }
769
770 #[test]
771 fn test_cache_stats_handler_with_stats() {
772 let stats = Arc::new(HttpCacheStats::default());
773 stats.record_hit();
774 stats.record_hit();
775 stats.record_miss();
776
777 let response = cache_stats_handler(Some(stats), "test-request-id");
778 assert_eq!(response.status(), StatusCode::OK);
779
780 let content_type = response.headers().get("Content-Type").unwrap();
781 assert_eq!(content_type, "application/json; charset=utf-8");
782 }
783
784 #[test]
785 fn test_cache_stats_handler_without_stats() {
786 let response = cache_stats_handler(None, "test-request-id");
787 assert_eq!(response.status(), StatusCode::OK);
788 }
789
790 #[test]
791 fn test_not_found_handler() {
792 let response = not_found_handler("test-request-id");
793 assert_eq!(response.status(), StatusCode::NOT_FOUND);
794 }
795
796 #[test]
797 fn test_config_handler_with_config() {
798 let config = Arc::new(Config::default_for_testing());
799 let response = config_handler(Some(config), "test-request-id");
800 assert_eq!(response.status(), StatusCode::OK);
801
802 let content_type = response.headers().get("Content-Type").unwrap();
803 assert_eq!(content_type, "application/json; charset=utf-8");
804 }
805
806 #[test]
807 fn test_config_handler_without_config() {
808 let response = config_handler(None, "test-request-id");
809 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
810 }
811
812 #[test]
813 fn test_upstreams_handler_with_data() {
814 let mut upstreams = HashMap::new();
815 upstreams.insert(
816 "backend".to_string(),
817 UpstreamStatus {
818 id: "backend".to_string(),
819 load_balancing: "round_robin".to_string(),
820 targets: vec![
821 TargetStatus {
822 address: "10.0.0.1:8080".to_string(),
823 weight: 1,
824 status: TargetHealthStatus::Healthy,
825 failure_rate: Some(0.0),
826 last_error: None,
827 },
828 TargetStatus {
829 address: "10.0.0.2:8080".to_string(),
830 weight: 1,
831 status: TargetHealthStatus::Unhealthy,
832 failure_rate: Some(0.8),
833 last_error: Some("connection refused".to_string()),
834 },
835 ],
836 },
837 );
838
839 let snapshot = UpstreamHealthSnapshot { upstreams };
840 let response = upstreams_handler(Some(snapshot), "test-request-id");
841 assert_eq!(response.status(), StatusCode::OK);
842
843 let content_type = response.headers().get("Content-Type").unwrap();
844 assert_eq!(content_type, "application/json; charset=utf-8");
845 }
846
847 #[test]
848 fn test_upstreams_handler_no_upstreams() {
849 let response = upstreams_handler(None, "test-request-id");
850 assert_eq!(response.status(), StatusCode::OK);
851 }
852
853 #[test]
854 fn test_uptime_formatting() {
855 let state = BuiltinHandlerState::new("0.1.0".to_string(), "test".to_string());
856
857 let uptime = state.uptime_string();
859 assert!(!uptime.is_empty());
860 }
861}