Skip to main content

max / makenotwork

4.8 KB · 169 lines History Blame Raw
1 //! Route accessibility checks, verify expected pages are reachable.
2
3 use std::time::{Duration, Instant};
4
5 use tracing::instrument;
6
7 /// Result of checking a single route.
8 #[derive(Debug, Clone)]
9 pub struct RouteCheckResult {
10 pub target: String,
11 pub path: String,
12 pub status_code: u16,
13 pub ok: bool,
14 pub checked_at: String,
15 pub response_time_ms: i64,
16 pub error: Option<String>,
17 }
18
19 /// Extract the base URL (scheme + host + port) from a health endpoint URL.
20 ///
21 /// ```text
22 /// "https://makenot.work/api/health" → "https://makenot.work"
23 /// "http://100.106.221.39:3400/api/health" → "http://100.106.221.39:3400"
24 /// ```
25 pub fn base_url_from_health_url(health_url: &str) -> Option<String> {
26 let rest = health_url
27 .strip_prefix("https://")
28 .map(|r| ("https", r))
29 .or_else(|| health_url.strip_prefix("http://").map(|r| ("http", r)))?;
30 let (scheme, after_scheme) = rest;
31 // Host (+ optional port) ends at the first '/' or end of string
32 let authority = after_scheme.split('/').next()?;
33 if authority.is_empty() {
34 return None;
35 }
36 Some(format!("{scheme}://{authority}"))
37 }
38
39 /// Check all expected routes for a target, returning one result per path.
40 ///
41 /// Runs sequentially within a target to avoid hammering the server.
42 #[instrument(skip_all, fields(target, route_count = paths.len()))]
43 pub async fn check_routes(
44 client: &reqwest::Client,
45 target: &str,
46 base_url: &str,
47 paths: &[String],
48 timeout: Duration,
49 ) -> Vec<RouteCheckResult> {
50 let mut results = Vec::with_capacity(paths.len());
51
52 for path in paths {
53 let url = format!("{base_url}{path}");
54 let checked_at = chrono::Utc::now().to_rfc3339();
55 let start = Instant::now();
56
57 let result = match client.get(&url).timeout(timeout).send().await {
58 Ok(response) => {
59 let status_code = response.status().as_u16();
60 let ok = response.status().is_success();
61 RouteCheckResult {
62 target: target.to_string(),
63 path: path.clone(),
64 status_code,
65 ok,
66 checked_at,
67 response_time_ms: start.elapsed().as_millis() as i64,
68 error: if ok {
69 None
70 } else {
71 Some(format!("HTTP {status_code}"))
72 },
73 }
74 }
75 Err(e) => {
76 let error_msg = if e.is_timeout() {
77 "timeout".to_string()
78 } else {
79 e.to_string()
80 };
81 RouteCheckResult {
82 target: target.to_string(),
83 path: path.clone(),
84 status_code: 0,
85 ok: false,
86 checked_at,
87 response_time_ms: start.elapsed().as_millis() as i64,
88 error: Some(error_msg),
89 }
90 }
91 };
92
93 results.push(result);
94 }
95
96 results
97 }
98
99 #[cfg(test)]
100 mod tests {
101 use super::*;
102
103 #[test]
104 fn base_url_from_https() {
105 assert_eq!(
106 base_url_from_health_url("https://makenot.work/api/health"),
107 Some("https://makenot.work".to_string()),
108 );
109 }
110
111 #[test]
112 fn base_url_from_http_with_port() {
113 assert_eq!(
114 base_url_from_health_url("http://100.106.221.39:3400/api/health"),
115 Some("http://100.106.221.39:3400".to_string()),
116 );
117 }
118
119 #[test]
120 fn base_url_from_invalid_url() {
121 assert_eq!(base_url_from_health_url("not-a-url"), None);
122 }
123
124 #[test]
125 fn base_url_from_localhost() {
126 assert_eq!(
127 base_url_from_health_url("http://127.0.0.1:9100/api/health"),
128 Some("http://127.0.0.1:9100".to_string()),
129 );
130 }
131
132 #[tokio::test]
133 async fn check_routes_handles_unreachable() {
134 let client = crate::tls::https_client_builder()
135 .timeout(Duration::from_millis(100))
136 .build()
137 .unwrap();
138
139 let results = check_routes(
140 &client,
141 "test",
142 "http://127.0.0.1:19999",
143 &["/".to_string()],
144 Duration::from_millis(100),
145 )
146 .await;
147
148 assert_eq!(results.len(), 1);
149 assert!(!results[0].ok);
150 assert_eq!(results[0].target, "test");
151 assert_eq!(results[0].path, "/");
152 assert!(results[0].error.is_some());
153 }
154
155 #[tokio::test]
156 async fn check_routes_empty_paths_returns_empty() {
157 let client = crate::tls::https_client_builder().build().unwrap();
158 let results = check_routes(
159 &client,
160 "test",
161 "http://127.0.0.1:19999",
162 &[],
163 Duration::from_millis(100),
164 )
165 .await;
166 assert!(results.is_empty());
167 }
168 }
169