//! Integration tests for the metrics HTTP server. //! //! These tests require `--features metrics`. #![allow(clippy::unwrap_used)] use std::io::{Read, Write}; use std::net::TcpStream; use std::time::Duration; use tidaldb::TidalDb; /// Make an HTTP GET request to the given address and path. /// Returns (status_code, body). fn http_get(addr: std::net::SocketAddr, path: &str) -> (u16, String) { let mut stream = TcpStream::connect(addr).expect("connect"); write!( stream, "GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" ) .unwrap(); stream.flush().unwrap(); let mut response = String::new(); stream.read_to_string(&mut response).unwrap(); // Parse status line: "HTTP/1.1 200 OK" let status: u16 = response .lines() .next() .and_then(|l| l.split_whitespace().nth(1)) .and_then(|s| s.parse().ok()) .unwrap_or(0); let body = response .splitn(2, "\r\n\r\n") .nth(1) .unwrap_or("") .to_string(); (status, body) } fn open_with_metrics() -> (std::net::SocketAddr, TidalDb) { let db = TidalDb::builder() .ephemeral() .enable_metrics("127.0.0.1:0") .open() .expect("open should succeed"); let addr = db .metrics_addr() .expect("metrics_addr should be Some when metrics enabled"); (addr, db) } #[test] fn metrics_server_starts_on_port_zero() { let (addr, db) = open_with_metrics(); assert_ne!(addr.port(), 0, "OS should have assigned a real port"); db.close().expect("close should succeed"); } #[test] fn healthz_returns_200_json() { let (addr, _db) = open_with_metrics(); let (status, body) = http_get(addr, "/healthz"); assert_eq!(status, 200); assert!(body.contains("\"status\":\"ok\""), "body: {body}"); assert!( body.contains("\"uptime_seconds\":"), "body should have uptime: {body}" ); assert!( body.contains("\"version\":"), "body should have version: {body}" ); assert!( body.contains("\"build_hash\":"), "body should have build_hash: {body}" ); } #[test] fn metrics_returns_200_prometheus() { let (addr, _db) = open_with_metrics(); let (status, body) = http_get(addr, "/metrics"); assert_eq!(status, 200); assert!( body.contains("tidaldb_uptime_seconds"), "missing uptime metric in body: {body}" ); assert!( body.contains("tidaldb_health_ok"), "missing health_ok metric in body: {body}" ); assert!( body.contains("tidaldb_info"), "missing info metric in body: {body}" ); } #[test] fn uptime_increases_over_time() { let (addr, _db) = open_with_metrics(); let (_, body1) = http_get(addr, "/metrics"); std::thread::sleep(Duration::from_millis(150)); let (_, body2) = http_get(addr, "/metrics"); let uptime1 = extract_uptime(&body1); let uptime2 = extract_uptime(&body2); assert!( uptime2 > uptime1, "uptime should increase: {uptime1} -> {uptime2}" ); } #[test] fn health_ok_value_is_one() { let (addr, _db) = open_with_metrics(); let (_, body) = http_get(addr, "/metrics"); let health_line = body .lines() .find(|l| l.starts_with("tidaldb_health_ok{")) .expect("should have health_ok metric"); assert!( health_line.ends_with(" 1"), "health should be 1: {health_line}" ); } #[test] fn not_found_returns_404() { let (addr, _db) = open_with_metrics(); let (status, _) = http_get(addr, "/nonexistent"); assert_eq!(status, 404); } #[test] fn close_stops_server() { let (addr, db) = open_with_metrics(); // Server should respond before close let (status, _) = http_get(addr, "/healthz"); assert_eq!(status, 200); db.close().expect("close should succeed"); // Give the thread a moment to shut down std::thread::sleep(Duration::from_millis(200)); // After close, connection should be refused let result = TcpStream::connect_timeout(&addr.into(), Duration::from_millis(200)); assert!(result.is_err(), "connection should be refused after close"); } // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- fn extract_uptime(prometheus_body: &str) -> f64 { for line in prometheus_body.lines() { if line.starts_with("tidaldb_uptime_seconds{") { let val = line.split_whitespace().last().unwrap_or("0"); return val.parse().unwrap_or(0.0); } } 0.0 }