kora_lib/validator/
cache_validator.rs1use deadpool_redis::Runtime;
2use redis::AsyncCommands;
3
4use crate::config::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(format!(
51 "Invalid cache_url format: '{cache_url}' - must start with redis:// or rediss://"
52 ));
53 }
54 }
55 }
56
57 if !usage_config.fallback_if_unavailable {
59 warnings.push(
60 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
61 .to_string(),
62 );
63 }
64
65 if let Some(cache_url) = &usage_config.cache_url {
67 if cache_url.starts_with("redis://") || cache_url.starts_with("rediss://") {
68 if let Err(e) = Self::test_redis_connection(cache_url).await {
69 if usage_config.fallback_if_unavailable {
70 warnings.push(format!(
71 "Usage limit Redis connection failed (fallback enabled): {e}"
72 ));
73 } else {
74 errors.push(format!(
75 "Usage limit Redis connection failed (fallback disabled): {e}"
76 ));
77 }
78 }
79 }
80 }
81
82 (errors, warnings)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::tests::config_mock::ConfigMockBuilder;
90 use serial_test::serial;
91
92 #[tokio::test]
93 #[serial]
94 async fn test_validate_usage_limit_disabled() {
95 let config = ConfigMockBuilder::new().with_usage_limit_enabled(false).build();
96
97 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
98
99 assert!(errors.is_empty());
100 assert!(warnings.is_empty());
101 }
102
103 #[tokio::test]
104 #[serial]
105 async fn test_validate_usage_limit_enabled_no_cache_url_fallback_enabled() {
106 let config = ConfigMockBuilder::new()
107 .with_usage_limit_enabled(true)
108 .with_usage_limit_cache_url(None)
109 .with_usage_limit_fallback(true)
110 .build();
111
112 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
113
114 assert!(errors.is_empty());
115 assert!(warnings.iter().any(
116 |w| w.contains("Usage limiting enabled without cache_url - using in-memory store")
117 ));
118 }
119
120 #[tokio::test]
121 #[serial]
122 async fn test_validate_usage_limit_enabled_no_cache_url_fallback_disabled() {
123 let config = ConfigMockBuilder::new()
124 .with_usage_limit_enabled(true)
125 .with_usage_limit_cache_url(None)
126 .with_usage_limit_fallback(false)
127 .build();
128
129 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
130
131 assert!(errors.is_empty());
133 assert!(warnings.iter().any(
134 |w| w.contains("Usage limiting enabled without cache_url - using in-memory store")
135 ));
136 assert!(warnings.iter().any(|w| w.contains(
138 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
139 )));
140 }
141
142 #[tokio::test]
143 #[serial]
144 async fn test_validate_usage_limit_invalid_cache_url_format() {
145 let config = ConfigMockBuilder::new()
146 .with_usage_limit_enabled(true)
147 .with_usage_limit_cache_url(Some("invalid://localhost:6379".to_string()))
148 .with_usage_limit_fallback(true)
149 .build();
150
151 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
152
153 assert!(errors.iter().any(|e| e.contains("Invalid cache_url format")
155 && e.contains("must start with redis:// or rediss://")));
156 assert!(!warnings.iter().any(|w| w.contains(
158 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
159 )));
160 }
161
162 #[tokio::test]
163 #[serial]
164 async fn test_validate_usage_limit_fallback_disabled_warning() {
165 let config = ConfigMockBuilder::new()
166 .with_usage_limit_enabled(true)
167 .with_usage_limit_cache_url(Some("redis://localhost:54321".to_string()))
170 .with_usage_limit_fallback(false)
171 .build();
172
173 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
174
175 assert!(errors
177 .iter()
178 .any(|e| e.contains("Usage limit Redis connection failed (fallback disabled)")));
179 assert!(warnings.iter().any(|w| w.contains(
180 "Usage limit fallback disabled - service will fail if cache becomes unavailable"
181 )));
182 }
183
184 #[tokio::test]
185 #[serial]
186 async fn test_validate_usage_limit_valid_redis_url() {
187 let config = ConfigMockBuilder::new()
188 .with_usage_limit_enabled(true)
189 .with_usage_limit_cache_url(Some("redis://localhost:54321".to_string()))
191 .with_usage_limit_fallback(true)
192 .build();
193
194 let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
195
196 assert!(errors.is_empty());
198 assert!(warnings
199 .iter()
200 .any(|w| w.contains("Usage limit Redis connection failed (fallback enabled)")));
201 }
202}