kora_lib/validator/
cache_validator.rs1use deadpool_redis::Runtime;
2use redis::AsyncCommands;
3
4use crate::config::{CacheConfig, UsageLimitConfig};
5
6pub struct CacheValidator {}
7
8impl CacheValidator {
9 async fn test_redis_connection(cache_url: &str) -> Result<(), String> {
11 let cfg = deadpool_redis::Config::from_url(cache_url);
12 let pool = cfg
13 .create_pool(Some(Runtime::Tokio1))
14 .map_err(|e| format!("Failed to create Redis pool: {e}"))?;
15
16 let mut conn = pool.get().await.map_err(|e| format!("Failed to connect to Redis: {e}"))?;
17
18 let _: Option<String> = conn
19 .get("__config_validator_test__")
20 .await
21 .map_err(|e| format!("Redis connection test failed: {e}"))?;
22
23 drop(conn);
24 drop(pool);
25
26 Ok(())
27 }
28
29 pub async fn validate(usage_config: &UsageLimitConfig) -> (Vec<String>, Vec<String>) {
30 let mut errors = Vec::new();
31 let mut warnings = Vec::new();
32
33 if !usage_config.enabled {
35 return (errors, warnings);
36 }
37
38 match &usage_config.cache_url {
40 None => {
41 warnings.push(
43 "Usage limiting enabled without cache_url - using in-memory store (not persistent across restarts)"
44 .to_string(),
45 );
46 }
47 Some(cache_url) => {
48 if !cache_url.starts_with("redis://") && !cache_url.starts_with("rediss://") {
50 errors.push(
51 "Invalid cache_url format: must start with redis:// or rediss://"
52 .to_string(),
53 );
54 }
55 }
56 }
57
58 if !usage_config.fallback_if_unavailable {
60 warnings.push(
61 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
62 .to_string(),
63 );
64 }
65
66 if let Some(cache_url) = &usage_config.cache_url {
68 if cache_url.starts_with("redis://") || cache_url.starts_with("rediss://") {
69 if let Err(e) = Self::test_redis_connection(cache_url).await {
70 if usage_config.fallback_if_unavailable {
71 warnings.push(format!(
72 "Usage limit Redis connection failed (fallback enabled): {e}"
73 ));
74 } else {
75 errors.push(format!(
76 "Usage limit Redis connection failed (fallback disabled): {e}"
77 ));
78 }
79 }
80 }
81 }
82
83 (errors, warnings)
84 }
85
86 pub async fn validate_rpc_cache(cache_config: &CacheConfig) -> (Vec<String>, Vec<String>) {
90 let mut errors = Vec::new();
91 let warnings: Vec<String> = Vec::new();
93
94 if !cache_config.enabled {
95 return (errors, warnings);
96 }
97
98 match &cache_config.url {
99 None => {
100 errors.push("RPC cache is enabled but no Redis URL is configured".to_string());
101 }
102 Some(cache_url) => {
103 if !cache_url.starts_with("redis://") && !cache_url.starts_with("rediss://") {
104 errors.push(
105 "Invalid cache_url format: must start with redis:// or rediss://"
106 .to_string(),
107 );
108 } else if let Err(_e) = Self::test_redis_connection(cache_url).await {
109 errors.push("RPC cache Redis connection failed".to_string());
110 }
111 }
112 }
113
114 (errors, warnings)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::tests::config_mock::ConfigMockBuilder;
122 use serial_test::serial;
123
124 #[tokio::test]
125 #[serial]
126 async fn test_validate_usage_limit_disabled() {
127 let config = ConfigMockBuilder::new().with_usage_limit_enabled(false).build();
128
129 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
130
131 assert!(errors.is_empty());
132 assert!(warnings.is_empty());
133 }
134
135 #[tokio::test]
136 #[serial]
137 async fn test_validate_usage_limit_enabled_no_cache_url_fallback_enabled() {
138 let config = ConfigMockBuilder::new()
139 .with_usage_limit_enabled(true)
140 .with_usage_limit_cache_url(None)
141 .with_usage_limit_fallback(true)
142 .build();
143
144 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
145
146 assert!(errors.is_empty());
147 assert!(warnings.iter().any(
148 |w| w.contains("Usage limiting enabled without cache_url - using in-memory store")
149 ));
150 }
151
152 #[tokio::test]
153 #[serial]
154 async fn test_validate_usage_limit_enabled_no_cache_url_fallback_disabled() {
155 let config = ConfigMockBuilder::new()
156 .with_usage_limit_enabled(true)
157 .with_usage_limit_cache_url(None)
158 .with_usage_limit_fallback(false)
159 .build();
160
161 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
162
163 assert!(errors.is_empty());
165 assert!(warnings.iter().any(
166 |w| w.contains("Usage limiting enabled without cache_url - using in-memory store")
167 ));
168 assert!(warnings.iter().any(|w| w.contains(
170 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
171 )));
172 }
173
174 #[tokio::test]
175 #[serial]
176 async fn test_validate_usage_limit_invalid_cache_url_format() {
177 let config = ConfigMockBuilder::new()
178 .with_usage_limit_enabled(true)
179 .with_usage_limit_cache_url(Some("invalid://localhost:6379".to_string()))
180 .with_usage_limit_fallback(true)
181 .build();
182
183 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
184
185 assert!(errors.iter().any(|e| e.contains("Invalid cache_url format")
187 && e.contains("must start with redis:// or rediss://")));
188 assert!(!warnings.iter().any(|w| w.contains(
190 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
191 )));
192 }
193
194 #[tokio::test]
195 #[serial]
196 async fn test_validate_usage_limit_fallback_disabled_warning() {
197 let config = ConfigMockBuilder::new()
198 .with_usage_limit_enabled(true)
199 .with_usage_limit_cache_url(Some("redis://localhost:54321".to_string()))
202 .with_usage_limit_fallback(false)
203 .build();
204
205 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
206
207 assert!(errors
209 .iter()
210 .any(|e| e.contains("Usage limit Redis connection failed (fallback disabled)")));
211 assert!(warnings.iter().any(|w| w.contains(
212 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
213 )));
214 }
215
216 #[tokio::test]
217 #[serial]
218 async fn test_validate_usage_limit_valid_redis_url() {
219 let config = ConfigMockBuilder::new()
220 .with_usage_limit_enabled(true)
221 .with_usage_limit_cache_url(Some("redis://localhost:54321".to_string()))
223 .with_usage_limit_fallback(true)
224 .build();
225
226 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
227
228 assert!(errors.is_empty());
230 assert!(warnings
231 .iter()
232 .any(|w| w.contains("Usage limit Redis connection failed (fallback enabled)")));
233 }
234
235 #[tokio::test]
237 #[serial]
238 async fn test_validate_rpc_cache_disabled() {
239 let config = ConfigMockBuilder::new().build();
240 let cache_config = CacheConfig {
241 enabled: false,
242 url: Some("redis://localhost:6379".to_string()),
243 ..config.kora.cache
244 };
245
246 let (errors, warnings) = CacheValidator::validate_rpc_cache(&cache_config).await;
247
248 assert!(errors.is_empty());
249 assert!(warnings.is_empty());
250 }
251
252 #[tokio::test]
254 #[serial]
255 async fn test_validate_rpc_cache_enabled_no_url() {
256 let config = ConfigMockBuilder::new().build();
257 let cache_config = CacheConfig { enabled: true, url: None, ..config.kora.cache };
258
259 let (errors, _) = CacheValidator::validate_rpc_cache(&cache_config).await;
260
261 assert!(!errors.is_empty());
262 assert!(errors[0].contains("no Redis URL"));
263 }
264
265 #[tokio::test]
267 #[serial]
268 async fn test_validate_rpc_cache_invalid_url_format() {
269 let config = ConfigMockBuilder::new().build();
270 let cache_config = CacheConfig {
271 enabled: true,
272 url: Some("http://localhost:6379".to_string()),
273 ..config.kora.cache
274 };
275
276 let (errors, _) = CacheValidator::validate_rpc_cache(&cache_config).await;
277
278 assert!(!errors.is_empty());
279 assert!(errors[0].contains("must start with redis://"));
280 }
281
282 #[tokio::test]
284 #[serial]
285 async fn test_validate_rpc_cache_connection_failed() {
286 let config = ConfigMockBuilder::new().build();
287 let cache_config = CacheConfig {
288 enabled: true,
289 url: Some("redis://localhost:54321".to_string()),
290 ..config.kora.cache
291 };
292
293 let (errors, _) = CacheValidator::validate_rpc_cache(&cache_config).await;
294
295 assert!(!errors.is_empty());
296 assert!(errors[0].contains("RPC cache Redis connection failed"));
297 }
298}