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.strip_prefix("https://")
27 .map(|r| ("https", r))
28 .or_else(|| health_url.strip_prefix("http://").map(|r| ("http", r)))?;
29 let (scheme, after_scheme) = rest;
30 // Host (+ optional port) ends at the first '/' or end of string
31 let authority = after_scheme.split('/').next()?;
32 if authority.is_empty() {
33 return None;
34 }
35 Some(format!("{scheme}://{authority}"))
36 }
37
38 /// Check all expected routes for a target, returning one result per path.
39 ///
40 /// Runs sequentially within a target to avoid hammering the server.
41 #[instrument(skip_all, fields(target, route_count = paths.len()))]
42 pub async fn check_routes(
43 client: &reqwest::Client,
44 target: &str,
45 base_url: &str,
46 paths: &[String],
47 timeout: Duration,
48 ) -> Vec<RouteCheckResult> {
49 let mut results = Vec::with_capacity(paths.len());
50
51 for path in paths {
52 let url = format!("{base_url}{path}");
53 let checked_at = chrono::Utc::now().to_rfc3339();
54 let start = Instant::now();
55
56 let result = match client
57 .get(&url)
58 .timeout(timeout)
59 .send()
60 .await
61 {
62 Ok(response) => {
63 let status_code = response.status().as_u16();
64 let ok = response.status().is_success();
65 RouteCheckResult {
66 target: target.to_string(),
67 path: path.clone(),
68 status_code,
69 ok,
70 checked_at,
71 response_time_ms: start.elapsed().as_millis() as i64,
72 error: if ok { None } else { Some(format!("HTTP {status_code}")) },
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 = reqwest::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 = reqwest::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