1use crate::admin::{AdminConfig, AdminState};
8use crate::backend::CacheBackend;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize, Deserialize)]
13pub struct HealthResponse {
14 pub status: String,
15 pub timestamp: String,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
20pub struct StatsResponse {
21 pub cache: CacheStats,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct CacheStats {
26 pub backend: String,
27 pub total_requests: u64,
28 pub hits: u64,
29 pub misses: u64,
30 pub stale_hits: u64,
31 pub stores: u64,
32 pub invalidations: u64,
33 pub hit_rate: f64,
34 pub miss_rate: f64,
35 pub uptime_seconds: u64,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
40pub struct HotKeysResponse {
41 pub hot_keys: Vec<HotKeyEntry>,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
45pub struct HotKeyEntry {
46 pub key: String,
47 pub hits: u64,
48 pub last_accessed: String,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
53pub struct TagsResponse {
54 pub tags: Vec<String>,
55 pub count: usize,
56}
57
58#[derive(Debug, Serialize, Deserialize)]
60pub struct InvalidationRequest {
61 pub key: Option<String>,
62 pub tag: Option<String>,
63 pub tags: Option<Vec<String>>,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
68pub struct InvalidationResponse {
69 pub success: bool,
70 pub keys_invalidated: usize,
71 pub message: String,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
76pub struct ErrorResponse {
77 pub error: String,
78 pub status: u16,
79}
80
81pub fn handle_health() -> HealthResponse {
83 HealthResponse {
84 status: "healthy".to_string(),
85 timestamp: chrono::Utc::now().to_rfc3339(),
86 }
87}
88
89pub fn handle_stats<B: CacheBackend>(state: &AdminState<B>) -> StatsResponse {
91 let snapshot = state.stats.snapshot();
92
93 StatsResponse {
94 cache: CacheStats {
95 backend: "cache".to_string(),
96 total_requests: snapshot.total_requests,
97 hits: snapshot.hits,
98 misses: snapshot.misses,
99 stale_hits: snapshot.stale_hits,
100 stores: snapshot.stores,
101 invalidations: snapshot.invalidations,
102 hit_rate: snapshot.hit_rate,
103 miss_rate: snapshot.miss_rate,
104 uptime_seconds: snapshot.uptime_seconds,
105 },
106 }
107}
108
109pub fn handle_hot_keys<B: CacheBackend>(
111 state: &AdminState<B>,
112 limit: Option<usize>,
113) -> HotKeysResponse {
114 let limit = limit.unwrap_or(state.config.max_hot_keys).min(100);
115 let hot_keys = state.stats.hot_keys(limit);
116
117 HotKeysResponse {
118 hot_keys: hot_keys
119 .into_iter()
120 .map(|k| HotKeyEntry {
121 key: k.key,
122 hits: k.hits,
123 last_accessed: chrono::DateTime::<chrono::Utc>::from(k.last_accessed).to_rfc3339(),
124 })
125 .collect(),
126 }
127}
128
129pub async fn handle_list_tags<B: CacheBackend>(
131 state: &AdminState<B>,
132) -> Result<TagsResponse, String> {
133 let tags = state
134 .backend
135 .list_tags()
136 .await
137 .map_err(|e| format!("Failed to list tags: {}", e))?;
138
139 let count = tags.len();
140
141 Ok(TagsResponse { tags, count })
142}
143
144pub async fn handle_invalidate<B: CacheBackend>(
146 state: &AdminState<B>,
147 request: InvalidationRequest,
148) -> Result<InvalidationResponse, String> {
149 if let Some(key) = request.key {
150 state
152 .backend
153 .invalidate(&key)
154 .await
155 .map_err(|e| format!("Failed to invalidate key: {}", e))?;
156
157 state.stats.record_invalidation();
158
159 Ok(InvalidationResponse {
160 success: true,
161 keys_invalidated: 1,
162 message: format!("Invalidated key: {}", key),
163 })
164 } else if let Some(tag) = request.tag {
165 let count = state
167 .backend
168 .invalidate_by_tag(&tag)
169 .await
170 .map_err(|e| format!("Failed to invalidate by tag: {}", e))?;
171
172 for _ in 0..count {
173 state.stats.record_invalidation();
174 }
175
176 Ok(InvalidationResponse {
177 success: true,
178 keys_invalidated: count,
179 message: format!("Invalidated {} keys with tag: {}", count, tag),
180 })
181 } else if let Some(tags) = request.tags {
182 let count = state
184 .backend
185 .invalidate_by_tags(&tags)
186 .await
187 .map_err(|e| format!("Failed to invalidate by tags: {}", e))?;
188
189 for _ in 0..count {
190 state.stats.record_invalidation();
191 }
192
193 Ok(InvalidationResponse {
194 success: true,
195 keys_invalidated: count,
196 message: format!("Invalidated {} keys", count),
197 })
198 } else {
199 Err("Must provide key, tag, or tags".to_string())
200 }
201}
202
203pub fn validate_auth(config: &AdminConfig, auth_header: Option<&str>) -> bool {
205 let token = auth_header.and_then(|h| {
206 h.strip_prefix("Bearer ").or(Some(h))
208 });
209
210 config.validate_token(token)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 use crate::backend::memory::InMemoryBackend;
218
219 fn test_state() -> AdminState<InMemoryBackend> {
220 let backend = InMemoryBackend::new(100);
221 let config = AdminConfig::new();
222 AdminState::new(backend, config)
223 }
224
225 #[test]
226 fn handle_health_returns_healthy() {
227 let response = handle_health();
228 assert_eq!(response.status, "healthy");
229 assert!(!response.timestamp.is_empty());
230 }
231
232 #[test]
233 fn handle_stats_returns_snapshot() {
234 let state = test_state();
235 state.stats.record_hit("key1");
236 state.stats.record_miss("key2");
237
238 let response = handle_stats(&state);
239 assert_eq!(response.cache.total_requests, 2);
240 assert_eq!(response.cache.hits, 1);
241 assert_eq!(response.cache.misses, 1);
242 }
243
244 #[test]
245 fn handle_hot_keys_returns_sorted_keys() {
246 let state = test_state();
247
248 for _ in 0..10 {
249 state.stats.record_hit("hot");
250 }
251 for _ in 0..5 {
252 state.stats.record_hit("warm");
253 }
254 state.stats.record_hit("cold");
255
256 let response = handle_hot_keys(&state, Some(2));
257 assert_eq!(response.hot_keys.len(), 2);
258 assert_eq!(response.hot_keys[0].key, "hot");
259 assert_eq!(response.hot_keys[0].hits, 10);
260 }
261
262 #[test]
263 fn handle_hot_keys_respects_limit() {
264 let state = test_state();
265
266 for i in 0..50 {
267 state.stats.record_hit(&format!("key{}", i));
268 }
269
270 let response = handle_hot_keys(&state, Some(10));
271 assert_eq!(response.hot_keys.len(), 10);
272
273 let response = handle_hot_keys(&state, Some(200));
275 assert!(response.hot_keys.len() <= 100);
276 }
277
278 #[tokio::test]
279 async fn handle_list_tags_returns_tags() {
280 let state = test_state();
281
282 let response = handle_list_tags(&state).await.unwrap();
284 assert_eq!(response.count, 0);
285 }
286
287 #[tokio::test]
288 async fn handle_invalidate_by_key() {
289 let state = test_state();
290
291 let request = InvalidationRequest {
292 key: Some("test_key".to_string()),
293 tag: None,
294 tags: None,
295 };
296
297 let response = handle_invalidate(&state, request).await.unwrap();
298 assert!(response.success);
299 assert_eq!(response.keys_invalidated, 1);
300 }
301
302 #[tokio::test]
303 async fn handle_invalidate_by_tag() {
304 let state = test_state();
305
306 let request = InvalidationRequest {
307 key: None,
308 tag: Some("user:123".to_string()),
309 tags: None,
310 };
311
312 let response = handle_invalidate(&state, request).await.unwrap();
313 assert!(response.success);
314 }
315
316 #[tokio::test]
317 async fn handle_invalidate_by_tags() {
318 let state = test_state();
319
320 let request = InvalidationRequest {
321 key: None,
322 tag: None,
323 tags: Some(vec!["tag1".to_string(), "tag2".to_string()]),
324 };
325
326 let response = handle_invalidate(&state, request).await.unwrap();
327 assert!(response.success);
328 }
329
330 #[tokio::test]
331 async fn handle_invalidate_requires_parameter() {
332 let state = test_state();
333
334 let request = InvalidationRequest {
335 key: None,
336 tag: None,
337 tags: None,
338 };
339
340 let result = handle_invalidate(&state, request).await;
341 assert!(result.is_err());
342 }
343
344 #[test]
345 fn validate_auth_with_bearer_token() {
346 let config = AdminConfig::new()
347 .with_auth_token("secret123")
348 .with_require_auth(true);
349
350 assert!(validate_auth(&config, Some("Bearer secret123")));
351 assert!(validate_auth(&config, Some("secret123")));
352 assert!(!validate_auth(&config, Some("Bearer wrong")));
353 assert!(!validate_auth(&config, None));
354 }
355
356 #[test]
357 fn validate_auth_no_auth_required() {
358 let config = AdminConfig::new().with_require_auth(false);
359
360 assert!(validate_auth(&config, None));
361 assert!(validate_auth(&config, Some("anything")));
362 }
363}