Skip to main content

litellm_rs/utils/data/validation/
data_validator.rs

1//! Data validation utilities
2
3use crate::utils::error::gateway_error::{GatewayError, Result};
4use regex::Regex;
5use std::collections::HashSet;
6
7/// Data validation utilities
8pub struct DataValidator;
9
10impl DataValidator {
11    /// Validate username
12    pub fn validate_username(username: &str) -> Result<()> {
13        if username.trim().is_empty() {
14            return Err(GatewayError::Validation(
15                "Username cannot be empty".to_string(),
16            ));
17        }
18
19        if username.len() < 3 {
20            return Err(GatewayError::Validation(
21                "Username must be at least 3 characters".to_string(),
22            ));
23        }
24
25        if username.len() > 50 {
26            return Err(GatewayError::Validation(
27                "Username cannot exceed 50 characters".to_string(),
28            ));
29        }
30
31        let username_regex = Regex::new(r"^[a-zA-Z0-9_-]+$")
32            .map_err(|e| GatewayError::Internal(format!("Regex error: {}", e)))?;
33
34        if !username_regex.is_match(username) {
35            return Err(GatewayError::Validation(
36                "Username can only contain letters, numbers, underscores, and hyphens".to_string(),
37            ));
38        }
39
40        Ok(())
41    }
42
43    /// Validate password strength
44    pub fn validate_password(password: &str) -> Result<()> {
45        if password.len() < 8 {
46            return Err(GatewayError::Validation(
47                "Password must be at least 8 characters".to_string(),
48            ));
49        }
50
51        if password.len() > 128 {
52            return Err(GatewayError::Validation(
53                "Password cannot exceed 128 characters".to_string(),
54            ));
55        }
56
57        let has_lowercase = password.chars().any(|c| c.is_lowercase());
58        let has_uppercase = password.chars().any(|c| c.is_uppercase());
59        let has_digit = password.chars().any(|c| c.is_ascii_digit());
60        let has_special = password
61            .chars()
62            .any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c));
63
64        let strength_count = [has_lowercase, has_uppercase, has_digit, has_special]
65            .iter()
66            .filter(|&&x| x)
67            .count();
68
69        if strength_count < 3 {
70            return Err(GatewayError::Validation(
71                "Password must contain at least 3 of: lowercase, uppercase, digit, special character".to_string()
72            ));
73        }
74
75        Ok(())
76    }
77
78    /// Validate team name
79    pub fn validate_team_name(name: &str) -> Result<()> {
80        if name.trim().is_empty() {
81            return Err(GatewayError::Validation(
82                "Team name cannot be empty".to_string(),
83            ));
84        }
85
86        if name.len() < 2 {
87            return Err(GatewayError::Validation(
88                "Team name must be at least 2 characters".to_string(),
89            ));
90        }
91
92        if name.len() > 100 {
93            return Err(GatewayError::Validation(
94                "Team name cannot exceed 100 characters".to_string(),
95            ));
96        }
97
98        Ok(())
99    }
100
101    /// Validate tags
102    pub fn validate_tags(tags: &[String]) -> Result<()> {
103        if tags.len() > 20 {
104            return Err(GatewayError::Validation(
105                "Cannot have more than 20 tags".to_string(),
106            ));
107        }
108
109        let mut unique_tags = HashSet::new();
110        for tag in tags {
111            if tag.trim().is_empty() {
112                return Err(GatewayError::Validation("Tag cannot be empty".to_string()));
113            }
114
115            if tag.len() > 50 {
116                return Err(GatewayError::Validation(
117                    "Tag cannot exceed 50 characters".to_string(),
118                ));
119            }
120
121            if !unique_tags.insert(tag.to_lowercase()) {
122                return Err(GatewayError::Validation(format!("Duplicate tag: {}", tag)));
123            }
124        }
125
126        Ok(())
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    // ==================== Username Validation Tests ====================
135
136    #[test]
137    fn test_validate_username_valid() {
138        assert!(DataValidator::validate_username("john_doe").is_ok());
139        assert!(DataValidator::validate_username("user123").is_ok());
140        assert!(DataValidator::validate_username("test-user").is_ok());
141        assert!(DataValidator::validate_username("ABC").is_ok());
142    }
143
144    #[test]
145    fn test_validate_username_empty() {
146        let result = DataValidator::validate_username("");
147        assert!(result.is_err());
148        assert!(result.unwrap_err().to_string().contains("empty"));
149    }
150
151    #[test]
152    fn test_validate_username_whitespace_only() {
153        let result = DataValidator::validate_username("   ");
154        assert!(result.is_err());
155        assert!(result.unwrap_err().to_string().contains("empty"));
156    }
157
158    #[test]
159    fn test_validate_username_too_short() {
160        let result = DataValidator::validate_username("ab");
161        assert!(result.is_err());
162        assert!(result.unwrap_err().to_string().contains("at least 3"));
163    }
164
165    #[test]
166    fn test_validate_username_exactly_3_chars() {
167        assert!(DataValidator::validate_username("abc").is_ok());
168    }
169
170    #[test]
171    fn test_validate_username_too_long() {
172        let long_name = "a".repeat(51);
173        let result = DataValidator::validate_username(&long_name);
174        assert!(result.is_err());
175        assert!(result.unwrap_err().to_string().contains("50"));
176    }
177
178    #[test]
179    fn test_validate_username_exactly_50_chars() {
180        let name = "a".repeat(50);
181        assert!(DataValidator::validate_username(&name).is_ok());
182    }
183
184    #[test]
185    fn test_validate_username_invalid_chars() {
186        let result = DataValidator::validate_username("user@name");
187        assert!(result.is_err());
188        assert!(
189            result
190                .unwrap_err()
191                .to_string()
192                .contains("letters, numbers, underscores")
193        );
194    }
195
196    #[test]
197    fn test_validate_username_space_in_middle() {
198        let result = DataValidator::validate_username("user name");
199        assert!(result.is_err());
200    }
201
202    #[test]
203    fn test_validate_username_special_chars() {
204        assert!(DataValidator::validate_username("user!name").is_err());
205        assert!(DataValidator::validate_username("user.name").is_err());
206        assert!(DataValidator::validate_username("user#name").is_err());
207    }
208
209    #[test]
210    fn test_validate_username_allowed_chars() {
211        assert!(DataValidator::validate_username("User_Name-123").is_ok());
212    }
213
214    // ==================== Password Validation Tests ====================
215
216    #[test]
217    fn test_validate_password_valid() {
218        assert!(DataValidator::validate_password("Password1!").is_ok());
219        assert!(DataValidator::validate_password("Secure@Pass123").is_ok());
220        assert!(DataValidator::validate_password("MyP@ssw0rd").is_ok());
221    }
222
223    #[test]
224    fn test_validate_password_too_short() {
225        let result = DataValidator::validate_password("Pass1!");
226        assert!(result.is_err());
227        assert!(result.unwrap_err().to_string().contains("at least 8"));
228    }
229
230    #[test]
231    fn test_validate_password_exactly_8_chars() {
232        assert!(DataValidator::validate_password("Pass1!ab").is_ok());
233    }
234
235    #[test]
236    fn test_validate_password_too_long() {
237        let long_pass = "a".repeat(129);
238        let result = DataValidator::validate_password(&long_pass);
239        assert!(result.is_err());
240        assert!(result.unwrap_err().to_string().contains("128"));
241    }
242
243    #[test]
244    fn test_validate_password_exactly_128_chars() {
245        // Need to meet strength requirements too
246        let pass = format!("Aa1!{}", "a".repeat(124));
247        assert!(DataValidator::validate_password(&pass).is_ok());
248    }
249
250    #[test]
251    fn test_validate_password_weak_lowercase_only() {
252        let result = DataValidator::validate_password("abcdefgh");
253        assert!(result.is_err());
254        assert!(result.unwrap_err().to_string().contains("at least 3"));
255    }
256
257    #[test]
258    fn test_validate_password_weak_no_special() {
259        // Lowercase + uppercase only = 2 strength
260        let result = DataValidator::validate_password("AbCdEfGh");
261        assert!(result.is_err());
262    }
263
264    #[test]
265    fn test_validate_password_three_strength() {
266        // Lowercase + uppercase + digit = 3 strength (valid)
267        assert!(DataValidator::validate_password("Password1").is_ok());
268    }
269
270    #[test]
271    fn test_validate_password_four_strength() {
272        // All four character types
273        assert!(DataValidator::validate_password("Password1!").is_ok());
274    }
275
276    #[test]
277    fn test_validate_password_digit_special_lowercase() {
278        assert!(DataValidator::validate_password("password1!").is_ok());
279    }
280
281    #[test]
282    fn test_validate_password_various_special_chars() {
283        assert!(DataValidator::validate_password("Password@1").is_ok());
284        assert!(DataValidator::validate_password("Password#1").is_ok());
285        assert!(DataValidator::validate_password("Password$1").is_ok());
286        assert!(DataValidator::validate_password("Password!1").is_ok());
287    }
288
289    // ==================== Team Name Validation Tests ====================
290
291    #[test]
292    fn test_validate_team_name_valid() {
293        assert!(DataValidator::validate_team_name("My Team").is_ok());
294        assert!(DataValidator::validate_team_name("Engineering").is_ok());
295        assert!(DataValidator::validate_team_name("AI Research Team").is_ok());
296    }
297
298    #[test]
299    fn test_validate_team_name_empty() {
300        let result = DataValidator::validate_team_name("");
301        assert!(result.is_err());
302        assert!(result.unwrap_err().to_string().contains("empty"));
303    }
304
305    #[test]
306    fn test_validate_team_name_whitespace_only() {
307        let result = DataValidator::validate_team_name("   ");
308        assert!(result.is_err());
309        assert!(result.unwrap_err().to_string().contains("empty"));
310    }
311
312    #[test]
313    fn test_validate_team_name_too_short() {
314        let result = DataValidator::validate_team_name("A");
315        assert!(result.is_err());
316        assert!(result.unwrap_err().to_string().contains("at least 2"));
317    }
318
319    #[test]
320    fn test_validate_team_name_exactly_2_chars() {
321        assert!(DataValidator::validate_team_name("AB").is_ok());
322    }
323
324    #[test]
325    fn test_validate_team_name_too_long() {
326        let long_name = "a".repeat(101);
327        let result = DataValidator::validate_team_name(&long_name);
328        assert!(result.is_err());
329        assert!(result.unwrap_err().to_string().contains("100"));
330    }
331
332    #[test]
333    fn test_validate_team_name_exactly_100_chars() {
334        let name = "a".repeat(100);
335        assert!(DataValidator::validate_team_name(&name).is_ok());
336    }
337
338    #[test]
339    fn test_validate_team_name_special_chars_allowed() {
340        // Team names can have special characters
341        assert!(DataValidator::validate_team_name("Team #1").is_ok());
342        assert!(DataValidator::validate_team_name("R&D Team").is_ok());
343        assert!(DataValidator::validate_team_name("Frontend/Backend").is_ok());
344    }
345
346    // ==================== Tags Validation Tests ====================
347
348    #[test]
349    fn test_validate_tags_empty_list() {
350        let tags: Vec<String> = vec![];
351        assert!(DataValidator::validate_tags(&tags).is_ok());
352    }
353
354    #[test]
355    fn test_validate_tags_valid() {
356        let tags = vec!["api".to_string(), "backend".to_string(), "v2".to_string()];
357        assert!(DataValidator::validate_tags(&tags).is_ok());
358    }
359
360    #[test]
361    fn test_validate_tags_too_many() {
362        let tags: Vec<String> = (0..21).map(|i| format!("tag{}", i)).collect();
363        let result = DataValidator::validate_tags(&tags);
364        assert!(result.is_err());
365        assert!(result.unwrap_err().to_string().contains("20"));
366    }
367
368    #[test]
369    fn test_validate_tags_exactly_20() {
370        let tags: Vec<String> = (0..20).map(|i| format!("tag{}", i)).collect();
371        assert!(DataValidator::validate_tags(&tags).is_ok());
372    }
373
374    #[test]
375    fn test_validate_tags_empty_tag() {
376        let tags = vec!["valid".to_string(), "".to_string()];
377        let result = DataValidator::validate_tags(&tags);
378        assert!(result.is_err());
379        assert!(result.unwrap_err().to_string().contains("empty"));
380    }
381
382    #[test]
383    fn test_validate_tags_whitespace_only_tag() {
384        let tags = vec!["valid".to_string(), "   ".to_string()];
385        let result = DataValidator::validate_tags(&tags);
386        assert!(result.is_err());
387        assert!(result.unwrap_err().to_string().contains("empty"));
388    }
389
390    #[test]
391    fn test_validate_tags_too_long_tag() {
392        let long_tag = "a".repeat(51);
393        let tags = vec![long_tag];
394        let result = DataValidator::validate_tags(&tags);
395        assert!(result.is_err());
396        assert!(result.unwrap_err().to_string().contains("50"));
397    }
398
399    #[test]
400    fn test_validate_tags_exactly_50_char_tag() {
401        let tag = "a".repeat(50);
402        let tags = vec![tag];
403        assert!(DataValidator::validate_tags(&tags).is_ok());
404    }
405
406    #[test]
407    fn test_validate_tags_duplicate() {
408        let tags = vec!["api".to_string(), "API".to_string()];
409        let result = DataValidator::validate_tags(&tags);
410        assert!(result.is_err());
411        assert!(result.unwrap_err().to_string().contains("Duplicate"));
412    }
413
414    #[test]
415    fn test_validate_tags_case_insensitive_duplicate() {
416        let tags = vec!["Backend".to_string(), "BACKEND".to_string()];
417        let result = DataValidator::validate_tags(&tags);
418        assert!(result.is_err());
419        assert!(result.unwrap_err().to_string().contains("Duplicate"));
420    }
421
422    #[test]
423    fn test_validate_tags_similar_but_different() {
424        let tags = vec!["api".to_string(), "api2".to_string(), "api-v2".to_string()];
425        assert!(DataValidator::validate_tags(&tags).is_ok());
426    }
427
428    #[test]
429    fn test_validate_tags_single_tag() {
430        let tags = vec!["single".to_string()];
431        assert!(DataValidator::validate_tags(&tags).is_ok());
432    }
433}