Skip to main content

kora_lib/validator/
cache_validator.rs

1use deadpool_redis::Runtime;
2use redis::AsyncCommands;
3
4use crate::config::UsageLimitConfig;
5
6pub struct CacheValidator {}
7
8impl CacheValidator {
9    /// Test Redis connection for usage limit cache
10    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        // Skip validation if usage limiting is disabled
34        if !usage_config.enabled {
35            return (errors, warnings);
36        }
37
38        // Check if cache_url is provided when enabled
39        match &usage_config.cache_url {
40            None => {
41                if !usage_config.fallback_if_unavailable {
42                    errors.push(
43                        "Usage limiting enabled without cache_url and fallback disabled - service will fail"
44                            .to_string(),
45                    );
46                } else {
47                    warnings.push(
48                        "Usage limiting enabled without cache_url - fallback mode will disable limits"
49                            .to_string(),
50                    );
51                }
52            }
53            Some(cache_url) => {
54                // Validate cache_url format
55                if !cache_url.starts_with("redis://") && !cache_url.starts_with("rediss://") {
56                    errors.push(format!(
57                        "Invalid cache_url format: '{cache_url}' - must start with redis:// or rediss://"
58                    ));
59                }
60            }
61        }
62
63        // Warn about fallback configuration
64        if !usage_config.fallback_if_unavailable {
65            warnings.push(
66                "Usage limit fallback disabled - service will fail if cache becomes unavailable"
67                    .to_string(),
68            );
69        }
70
71        // Test Redis connection
72        if let Some(cache_url) = &usage_config.cache_url {
73            if cache_url.starts_with("redis://") || cache_url.starts_with("rediss://") {
74                match Self::test_redis_connection(cache_url).await {
75                    Ok(_) => {}
76                    Err(e) => {
77                        if usage_config.fallback_if_unavailable {
78                            warnings.push(format!(
79                                "Usage limit Redis connection failed (fallback enabled): {e}"
80                            ));
81                        } else {
82                            errors.push(format!(
83                                "Usage limit Redis connection failed (fallback disabled): {e}"
84                            ));
85                        }
86                    }
87                };
88            }
89        }
90
91        (errors, warnings)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::tests::config_mock::ConfigMockBuilder;
99    use serial_test::serial;
100
101    #[tokio::test]
102    #[serial]
103    async fn test_validate_usage_limit_disabled() {
104        let config = ConfigMockBuilder::new().with_usage_limit_enabled(false).build();
105
106        let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
107
108        assert!(errors.is_empty());
109        assert!(warnings.is_empty());
110    }
111
112    #[tokio::test]
113    #[serial]
114    async fn test_validate_usage_limit_enabled_no_cache_url_fallback_enabled() {
115        let config = ConfigMockBuilder::new()
116            .with_usage_limit_enabled(true)
117            .with_usage_limit_cache_url(None)
118            .with_usage_limit_fallback(true)
119            .build();
120
121        let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
122
123        assert!(errors.is_empty());
124        assert!(warnings.iter().any(|w| w.contains(
125            "Usage limiting enabled without cache_url - fallback mode will disable limits"
126        )));
127    }
128
129    #[tokio::test]
130    #[serial]
131    async fn test_validate_usage_limit_enabled_no_cache_url_fallback_disabled() {
132        let config = ConfigMockBuilder::new()
133            .with_usage_limit_enabled(true)
134            .with_usage_limit_cache_url(None)
135            .with_usage_limit_fallback(false)
136            .build();
137
138        let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
139
140        // Should error when no cache_url and fallback disabled
141        assert!(errors.iter().any(|e| e.contains(
142            "Usage limiting enabled without cache_url and fallback disabled - service will fail"
143        )));
144        assert!(warnings.iter().any(|w| w.contains(
145            "Usage limit fallback disabled - service will fail if cache becomes unavailable"
146        )));
147    }
148
149    #[tokio::test]
150    #[serial]
151    async fn test_validate_usage_limit_invalid_cache_url_format() {
152        let config = ConfigMockBuilder::new()
153            .with_usage_limit_enabled(true)
154            .with_usage_limit_cache_url(Some("invalid://localhost:6379".to_string()))
155            .with_usage_limit_fallback(true)
156            .build();
157
158        let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
159
160        // Should error for invalid cache_url format
161        assert!(errors.iter().any(|e| e.contains("Invalid cache_url format")
162            && e.contains("must start with redis:// or rediss://")));
163        // No fallback warning since fallback is enabled
164        assert!(!warnings.iter().any(|w| w.contains(
165            "Usage limit fallback disabled - service will fail if cache becomes unavailable"
166        )));
167    }
168
169    #[tokio::test]
170    #[serial]
171    async fn test_validate_usage_limit_fallback_disabled_warning() {
172        let config = ConfigMockBuilder::new()
173            .with_usage_limit_enabled(true)
174            .with_usage_limit_cache_url(Some("redis://localhost:6379".to_string()))
175            .with_usage_limit_fallback(false)
176            .build();
177
178        let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
179
180        // Should error about Redis connection failure with fallback disabled
181        assert!(errors
182            .iter()
183            .any(|e| e.contains("Usage limit Redis connection failed (fallback disabled)")));
184        assert!(warnings.iter().any(|w| w.contains(
185            "Usage limit fallback disabled - service will fail if cache becomes unavailable"
186        )));
187    }
188
189    #[tokio::test]
190    #[serial]
191    async fn test_validate_usage_limit_valid_redis_url() {
192        let config = ConfigMockBuilder::new()
193            .with_usage_limit_enabled(true)
194            .with_usage_limit_cache_url(Some("redis://localhost:6379".to_string()))
195            .with_usage_limit_fallback(true)
196            .build();
197
198        let (errors, warnings) = CacheValidator::validate(&config.kora.usage_limit).await;
199
200        // Should get warnings because Redis connection fails (unit tests don't run Redis) but fallback is enabled
201        assert!(errors.is_empty());
202        assert!(warnings
203            .iter()
204            .any(|w| w.contains("Usage limit Redis connection failed (fallback enabled)")));
205    }
206}