Skip to main content

fraiseql_core/validation/
id_policy.rs

1//! ID Policy validation for GraphQL ID scalar type
2//!
3//! This module provides validation for ID fields based on the configured ID policy.
4//!
5//! **Design Pattern**: `FraiseQL` supports two ID policies:
6//! 1. **UUID**: IDs must be valid UUIDs (`FraiseQL`'s opinionated default)
7//! 2. **OPAQUE**: IDs accept any string (GraphQL spec-compliant)
8//!
9//! This module enforces UUID format validation when `IDPolicy::UUID` is configured.
10//!
11//! # Example
12//!
13//! ```
14//! use fraiseql_core::validation::{IDPolicy, validate_id};
15//!
16//! // UUID policy: strict UUID validation
17//! let policy = IDPolicy::UUID;
18//! assert!(validate_id("550e8400-e29b-41d4-a716-446655440000", policy).is_ok());
19//! assert!(validate_id("not-a-uuid", policy).is_err());
20//!
21//! // OPAQUE policy: any string accepted
22//! let policy = IDPolicy::OPAQUE;
23//! assert!(validate_id("not-a-uuid", policy).is_ok());
24//! assert!(validate_id("any-arbitrary-string", policy).is_ok());
25//! ```
26
27use serde::{Deserialize, Serialize};
28
29/// ID Policy determines how GraphQL ID scalar type behaves
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
31pub enum IDPolicy {
32    /// IDs must be valid UUIDs (`FraiseQL`'s opinionated default)
33    #[serde(rename = "uuid")]
34    #[default]
35    UUID,
36
37    /// IDs accept any string (GraphQL specification compliant)
38    #[serde(rename = "opaque")]
39    OPAQUE,
40}
41
42impl IDPolicy {
43    /// Check if this policy enforces UUID format for IDs
44    #[must_use]
45    pub fn enforces_uuid(self) -> bool {
46        self == Self::UUID
47    }
48
49    /// Get the policy name as a string
50    #[must_use]
51    pub const fn as_str(self) -> &'static str {
52        match self {
53            Self::UUID => "uuid",
54            Self::OPAQUE => "opaque",
55        }
56    }
57}
58
59impl std::fmt::Display for IDPolicy {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "{}", self.as_str())
62    }
63}
64
65/// Error type for ID validation failures
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct IDValidationError {
68    /// The invalid ID value
69    pub value:   String,
70    /// The policy that was violated
71    pub policy:  IDPolicy,
72    /// Error message
73    pub message: String,
74}
75
76impl std::fmt::Display for IDValidationError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}", self.message)
79    }
80}
81
82impl std::error::Error for IDValidationError {}
83
84/// Validate an ID string against the configured ID policy
85///
86/// # Arguments
87///
88/// * `id` - The ID value to validate
89/// * `policy` - The ID policy to enforce
90///
91/// # Returns
92///
93/// `Ok(())` if the ID is valid for the policy, `Err(IDValidationError)` otherwise
94///
95/// # Errors
96///
97/// Returns `IDValidationError` if the ID does not conform to the specified policy.
98/// For `IDPolicy::UUID`, the ID must be a valid UUID. For `IDPolicy::OPAQUE`, any string is valid.
99///
100/// # Examples
101///
102/// ```
103/// use fraiseql_core::validation::{IDPolicy, validate_id};
104///
105/// // UUID policy enforces UUID format
106/// assert!(validate_id("550e8400-e29b-41d4-a716-446655440000", IDPolicy::UUID).is_ok());
107/// assert!(validate_id("not-uuid", IDPolicy::UUID).is_err());
108///
109/// // OPAQUE policy accepts any string
110/// assert!(validate_id("anything", IDPolicy::OPAQUE).is_ok());
111/// assert!(validate_id("", IDPolicy::OPAQUE).is_ok());
112/// ```
113pub fn validate_id(id: &str, policy: IDPolicy) -> Result<(), IDValidationError> {
114    match policy {
115        IDPolicy::UUID => validate_uuid_format(id),
116        IDPolicy::OPAQUE => Ok(()), // Opaque IDs accept any string
117    }
118}
119
120/// Validate that an ID is a valid UUID string
121///
122/// **Security Note**: This validation happens at the Rust layer for defense-in-depth.
123/// Python layer validation via `IDPolicy` is the primary enforcement mechanism.
124///
125/// UUID format validation requires:
126/// - 36 characters total
127/// - 8-4-4-4-12 hexadecimal digits separated by hyphens
128/// - Case-insensitive
129///
130/// # Arguments
131///
132/// * `id` - The ID string to validate
133///
134/// # Returns
135///
136/// `Ok(())` if valid UUID format, `Err(IDValidationError)` otherwise
137fn validate_uuid_format(id: &str) -> Result<(), IDValidationError> {
138    // UUID must be 36 characters: 8-4-4-4-12
139    if id.len() != 36 {
140        return Err(IDValidationError {
141            value:   id.to_string(),
142            policy:  IDPolicy::UUID,
143            message: format!(
144                "ID must be a valid UUID (36 characters), got {} characters",
145                id.len()
146            ),
147        });
148    }
149
150    // Check overall structure: 8-4-4-4-12
151    let parts: Vec<&str> = id.split('-').collect();
152    if parts.len() != 5 {
153        return Err(IDValidationError {
154            value:   id.to_string(),
155            policy:  IDPolicy::UUID,
156            message: "ID must be a valid UUID with format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
157                .to_string(),
158        });
159    }
160
161    // Validate segment lengths
162    let expected_lengths = [8, 4, 4, 4, 12];
163    for (i, (part, &expected_len)) in parts.iter().zip(&expected_lengths).enumerate() {
164        if part.len() != expected_len {
165            return Err(IDValidationError {
166                value:   id.to_string(),
167                policy:  IDPolicy::UUID,
168                message: format!(
169                    "UUID segment {} has invalid length: expected {}, got {}",
170                    i,
171                    expected_len,
172                    part.len()
173                ),
174            });
175        }
176    }
177
178    // Validate all characters are hexadecimal
179    for (i, part) in parts.iter().enumerate() {
180        if !part.chars().all(|c| c.is_ascii_hexdigit()) {
181            return Err(IDValidationError {
182                value:   id.to_string(),
183                policy:  IDPolicy::UUID,
184                message: format!("UUID segment {i} contains non-hexadecimal characters: '{part}'"),
185            });
186        }
187    }
188
189    Ok(())
190}
191
192/// Validate multiple IDs against a policy
193///
194/// # Arguments
195///
196/// * `ids` - Slice of ID strings to validate
197/// * `policy` - The ID policy to enforce
198///
199/// # Returns
200///
201/// `Ok(())` if all IDs are valid, `Err(IDValidationError)` for the first invalid ID
202///
203/// # Examples
204///
205/// ```ignore
206/// use fraiseql_core::validation::{IDPolicy, validate_ids};
207///
208/// let ids = vec![
209///     "550e8400-e29b-41d4-a716-446655440000",
210///     "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
211/// ];
212/// assert!(validate_ids(&ids, IDPolicy::UUID).is_ok());
213/// ```
214///
215/// # Errors
216///
217/// Returns `IDValidationError` if any ID fails validation.
218#[allow(dead_code)] // Reason: public API intended for external consumers; no in-crate callers yet
219pub fn validate_ids(ids: &[&str], policy: IDPolicy) -> Result<(), IDValidationError> {
220    for id in ids {
221        validate_id(id, policy)?;
222    }
223    Ok(())
224}
225
226// =============================================================================
227// Pluggable ID Validator Trait System
228// =============================================================================
229
230/// Trait for pluggable ID validation strategies
231///
232/// This trait enables users to implement custom ID validation logic
233/// beyond the built-in UUID and OPAQUE policies.
234///
235/// # Examples
236///
237/// ```ignore
238/// use fraiseql_core::validation::IdValidator;
239///
240/// struct CustomIdValidator;
241///
242/// impl IdValidator for CustomIdValidator {
243///     fn validate(&self, value: &str) -> Result<(), IDValidationError> {
244///         if value.starts_with("CUSTOM-") {
245///             Ok(())
246///         } else {
247///             Err(IDValidationError {
248///                 value: value.to_string(),
249///                 policy: IDPolicy::OPAQUE,
250///                 message: "Custom IDs must start with 'CUSTOM-'".to_string(),
251///             })
252///         }
253///     }
254///
255///     fn format_name(&self) -> &'static str {
256///         "CUSTOM"
257///     }
258/// }
259/// ```
260pub trait IdValidator: Send + Sync {
261    /// Validate an ID value
262    fn validate(&self, value: &str) -> Result<(), IDValidationError>;
263
264    /// Human-readable name of the format (for error messages)
265    fn format_name(&self) -> &'static str;
266}
267
268/// UUID format validator
269#[derive(Debug, Clone, Copy)]
270pub struct UuidIdValidator;
271
272impl IdValidator for UuidIdValidator {
273    fn validate(&self, value: &str) -> Result<(), IDValidationError> {
274        validate_uuid_format(value)
275    }
276
277    fn format_name(&self) -> &'static str {
278        "UUID"
279    }
280}
281
282/// Numeric ID validator (integers)
283#[derive(Debug, Clone, Copy)]
284pub struct NumericIdValidator;
285
286impl IdValidator for NumericIdValidator {
287    fn validate(&self, value: &str) -> Result<(), IDValidationError> {
288        value.parse::<i64>().map_err(|_| IDValidationError {
289            value:   value.to_string(),
290            policy:  IDPolicy::OPAQUE,
291            message: format!(
292                "ID must be a valid {} (parseable as 64-bit integer)",
293                self.format_name()
294            ),
295        })?;
296        Ok(())
297    }
298
299    fn format_name(&self) -> &'static str {
300        "integer"
301    }
302}
303
304/// ULID format validator (Universally Unique Lexicographically Sortable Identifier)
305///
306/// ULIDs are 26 uppercase alphanumeric characters, providing sortable unique IDs.
307/// Example: `01ARZ3NDEKTSV4RRFFQ69G5FAV`
308#[derive(Debug, Clone, Copy)]
309pub struct UlidIdValidator;
310
311impl IdValidator for UlidIdValidator {
312    fn validate(&self, value: &str) -> Result<(), IDValidationError> {
313        if value.len() != 26 {
314            return Err(IDValidationError {
315                value:   value.to_string(),
316                policy:  IDPolicy::OPAQUE,
317                message: format!(
318                    "ID must be a valid {} ({} characters), got {}",
319                    self.format_name(),
320                    26,
321                    value.len()
322                ),
323            });
324        }
325
326        // ULIDs use Crockford base32 encoding (0-9, A-Z except I, L, O, U)
327        if !value.chars().all(|c| {
328            c.is_ascii_digit()
329                || (c.is_ascii_uppercase() && c != 'I' && c != 'L' && c != 'O' && c != 'U')
330        }) {
331            return Err(IDValidationError {
332                value:   value.to_string(),
333                policy:  IDPolicy::OPAQUE,
334                message: format!(
335                    "ID must be a valid {} (Crockford base32: 0-9, A-Z except I, L, O, U)",
336                    self.format_name()
337                ),
338            });
339        }
340
341        Ok(())
342    }
343
344    fn format_name(&self) -> &'static str {
345        "ULID"
346    }
347}
348
349/// Opaque ID validator (accepts any string)
350#[derive(Debug, Clone, Copy)]
351pub struct OpaqueIdValidator;
352
353impl IdValidator for OpaqueIdValidator {
354    fn validate(&self, _value: &str) -> Result<(), IDValidationError> {
355        Ok(()) // Accept any string
356    }
357
358    fn format_name(&self) -> &'static str {
359        "opaque"
360    }
361}
362
363/// ID validation profile for different use cases
364///
365/// Profiles provide preset ID validation configurations for common scenarios.
366/// Each profile includes a name and a validator instance.
367///
368/// # Built-in Profiles
369///
370/// - **UUID**: Strict UUID format validation (FraiseQL default)
371/// - **Numeric**: Integer-based IDs (suitable for sequential IDs)
372/// - **ULID**: Sortable unique identifiers (recommended for distributed systems)
373/// - **Opaque**: Any string accepted (GraphQL spec compliant)
374#[derive(Debug, Clone)]
375pub struct IDValidationProfile {
376    /// Profile name (e.g., "uuid", "ulid", "numeric")
377    pub name: String,
378
379    /// Validator instance for this profile
380    pub validator: ValidationProfileType,
381}
382
383/// Type of validation profile
384#[derive(Debug, Clone)]
385pub enum ValidationProfileType {
386    /// UUID format validation
387    Uuid(UuidIdValidator),
388
389    /// Numeric (integer) validation
390    Numeric(NumericIdValidator),
391
392    /// ULID format validation
393    Ulid(UlidIdValidator),
394
395    /// Opaque (any string) validation
396    Opaque(OpaqueIdValidator),
397}
398
399impl ValidationProfileType {
400    /// Get the validator as a trait object
401    pub fn as_validator(&self) -> &dyn IdValidator {
402        match self {
403            Self::Uuid(v) => v,
404            Self::Numeric(v) => v,
405            Self::Ulid(v) => v,
406            Self::Opaque(v) => v,
407        }
408    }
409}
410
411impl IDValidationProfile {
412    /// Create a UUID validation profile (FraiseQL default)
413    #[must_use]
414    pub fn uuid() -> Self {
415        Self {
416            name:      "uuid".to_string(),
417            validator: ValidationProfileType::Uuid(UuidIdValidator),
418        }
419    }
420
421    /// Create a numeric (integer) validation profile
422    #[must_use]
423    pub fn numeric() -> Self {
424        Self {
425            name:      "numeric".to_string(),
426            validator: ValidationProfileType::Numeric(NumericIdValidator),
427        }
428    }
429
430    /// Create a ULID validation profile
431    #[must_use]
432    pub fn ulid() -> Self {
433        Self {
434            name:      "ulid".to_string(),
435            validator: ValidationProfileType::Ulid(UlidIdValidator),
436        }
437    }
438
439    /// Create an opaque (any string) validation profile
440    #[must_use]
441    pub fn opaque() -> Self {
442        Self {
443            name:      "opaque".to_string(),
444            validator: ValidationProfileType::Opaque(OpaqueIdValidator),
445        }
446    }
447
448    /// Get profile by name
449    ///
450    /// Returns a profile matching the given name, or None if not found.
451    ///
452    /// # Built-in Profile Names
453    ///
454    /// - "uuid" - UUID validation
455    /// - "numeric" - Integer validation
456    /// - "ulid" - ULID validation
457    /// - "opaque" - Any string validation
458    #[must_use]
459    pub fn by_name(name: &str) -> Option<Self> {
460        match name.to_lowercase().as_str() {
461            "uuid" => Some(Self::uuid()),
462            "numeric" | "integer" => Some(Self::numeric()),
463            "ulid" => Some(Self::ulid()),
464            "opaque" | "string" => Some(Self::opaque()),
465            _ => None,
466        }
467    }
468
469    /// Validate an ID using this profile
470    pub fn validate(&self, value: &str) -> Result<(), IDValidationError> {
471        self.validator.as_validator().validate(value)
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    // ==================== UUID Format Tests ====================
480
481    #[test]
482    fn test_validate_valid_uuid() {
483        // Standard UUID format
484        let result = validate_id("550e8400-e29b-41d4-a716-446655440000", IDPolicy::UUID);
485        assert!(result.is_ok());
486    }
487
488    #[test]
489    fn test_validate_valid_uuid_uppercase() {
490        // UUIDs are case-insensitive
491        let result = validate_id("550E8400-E29B-41D4-A716-446655440000", IDPolicy::UUID);
492        assert!(result.is_ok());
493    }
494
495    #[test]
496    fn test_validate_valid_uuid_mixed_case() {
497        let result = validate_id("550e8400-E29b-41d4-A716-446655440000", IDPolicy::UUID);
498        assert!(result.is_ok());
499    }
500
501    #[test]
502    fn test_validate_nil_uuid() {
503        // Nil UUID (all zeros) is valid
504        let result = validate_id("00000000-0000-0000-0000-000000000000", IDPolicy::UUID);
505        assert!(result.is_ok());
506    }
507
508    #[test]
509    fn test_validate_max_uuid() {
510        // Max UUID (all Fs) is valid
511        let result = validate_id("ffffffff-ffff-ffff-ffff-ffffffffffff", IDPolicy::UUID);
512        assert!(result.is_ok());
513    }
514
515    #[test]
516    fn test_validate_uuid_wrong_length() {
517        let result = validate_id("550e8400-e29b-41d4-a716", IDPolicy::UUID);
518        assert!(result.is_err());
519        let err = result.unwrap_err();
520        assert_eq!(err.policy, IDPolicy::UUID);
521        assert!(err.message.contains("36 characters"));
522    }
523
524    #[test]
525    fn test_validate_uuid_extra_chars() {
526        let result = validate_id("550e8400-e29b-41d4-a716-446655440000x", IDPolicy::UUID);
527        assert!(result.is_err());
528    }
529
530    #[test]
531    fn test_validate_uuid_missing_hyphens() {
532        // 36 chars without hyphens - all hex digits, same length as UUID but no separators
533        let result = validate_id("550e8400e29b41d4a716446655440000", IDPolicy::UUID);
534        assert!(result.is_err());
535        let err = result.unwrap_err();
536        // Fails length check since 32 chars != 36
537        assert!(err.message.contains("36 characters"));
538    }
539
540    #[test]
541    fn test_validate_uuid_wrong_segment_lengths() {
542        // First segment too short (7 chars instead of 8)
543        // Need 36 chars total, so pad the last segment: 550e840-e29b-41d4-a716-4466554400001
544        let result = validate_id("550e840-e29b-41d4-a716-4466554400001", IDPolicy::UUID);
545        assert!(result.is_err());
546        let err = result.unwrap_err();
547        assert!(err.message.contains("segment"));
548    }
549
550    #[test]
551    fn test_validate_uuid_non_hex_chars() {
552        let result = validate_id("550e8400-e29b-41d4-a716-44665544000g", IDPolicy::UUID);
553        assert!(result.is_err());
554        let err = result.unwrap_err();
555        assert!(err.message.contains("non-hexadecimal"));
556    }
557
558    #[test]
559    fn test_validate_uuid_special_chars() {
560        let result = validate_id("550e8400-e29b-41d4-a716-4466554400@0", IDPolicy::UUID);
561        assert!(result.is_err());
562    }
563
564    #[test]
565    fn test_validate_uuid_empty_string() {
566        let result = validate_id("", IDPolicy::UUID);
567        assert!(result.is_err());
568    }
569
570    // ==================== OPAQUE Policy Tests ====================
571
572    #[test]
573    fn test_opaque_accepts_any_string() {
574        assert!(validate_id("not-a-uuid", IDPolicy::OPAQUE).is_ok());
575        assert!(validate_id("anything", IDPolicy::OPAQUE).is_ok());
576        assert!(validate_id("12345", IDPolicy::OPAQUE).is_ok());
577        assert!(validate_id("special@chars!#$%", IDPolicy::OPAQUE).is_ok());
578    }
579
580    #[test]
581    fn test_opaque_accepts_empty_string() {
582        assert!(validate_id("", IDPolicy::OPAQUE).is_ok());
583    }
584
585    #[test]
586    fn test_opaque_accepts_uuid() {
587        assert!(validate_id("550e8400-e29b-41d4-a716-446655440000", IDPolicy::OPAQUE).is_ok());
588    }
589
590    // ==================== Multiple IDs Tests ====================
591
592    #[test]
593    fn test_validate_multiple_valid_uuids() {
594        let ids = vec![
595            "550e8400-e29b-41d4-a716-446655440000",
596            "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
597            "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
598        ];
599        assert!(validate_ids(&ids, IDPolicy::UUID).is_ok());
600    }
601
602    #[test]
603    fn test_validate_multiple_fails_on_first_invalid() {
604        let ids = vec![
605            "550e8400-e29b-41d4-a716-446655440000",
606            "invalid-id",
607            "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
608        ];
609        let result = validate_ids(&ids, IDPolicy::UUID);
610        assert!(result.is_err());
611        assert_eq!(result.unwrap_err().value, "invalid-id");
612    }
613
614    #[test]
615    fn test_validate_multiple_opaque_all_pass() {
616        let ids = vec!["anything", "goes", "here", "12345"];
617        assert!(validate_ids(&ids, IDPolicy::OPAQUE).is_ok());
618    }
619
620    // ==================== Policy Behavior Tests ====================
621
622    #[test]
623    fn test_policy_enforces_uuid() {
624        assert!(IDPolicy::UUID.enforces_uuid());
625        assert!(!IDPolicy::OPAQUE.enforces_uuid());
626    }
627
628    #[test]
629    fn test_policy_as_str() {
630        assert_eq!(IDPolicy::UUID.as_str(), "uuid");
631        assert_eq!(IDPolicy::OPAQUE.as_str(), "opaque");
632    }
633
634    #[test]
635    fn test_policy_default() {
636        assert_eq!(IDPolicy::default(), IDPolicy::UUID);
637    }
638
639    #[test]
640    fn test_policy_display() {
641        assert_eq!(format!("{}", IDPolicy::UUID), "uuid");
642        assert_eq!(format!("{}", IDPolicy::OPAQUE), "opaque");
643    }
644
645    // ==================== Security Scenarios ====================
646
647    #[test]
648    fn test_security_prevent_sql_injection_via_uuid() {
649        // UUID validation prevents malicious IDs with SQL injection
650        let result = validate_id("'; DROP TABLE users; --", IDPolicy::UUID);
651        assert!(result.is_err());
652    }
653
654    #[test]
655    fn test_security_prevent_path_traversal_via_uuid() {
656        let result = validate_id("../../etc/passwd", IDPolicy::UUID);
657        assert!(result.is_err());
658    }
659
660    #[test]
661    fn test_security_opaque_policy_accepts_any_format() {
662        // OPAQUE policy explicitly accepts any string
663        // Input validation and authorization must be done elsewhere
664        assert!(validate_id("'; DROP TABLE users; --", IDPolicy::OPAQUE).is_ok());
665        assert!(validate_id("../../etc/passwd", IDPolicy::OPAQUE).is_ok());
666    }
667
668    #[test]
669    fn test_validation_error_contains_policy_info() {
670        let err = validate_id("invalid", IDPolicy::UUID).unwrap_err();
671        assert_eq!(err.policy, IDPolicy::UUID);
672        assert_eq!(err.value, "invalid");
673        assert!(!err.message.is_empty());
674    }
675
676    // ==================== UUID Validator Tests ====================
677
678    #[test]
679    fn test_uuid_validator_valid() {
680        let validator = UuidIdValidator;
681        let result = validator.validate("550e8400-e29b-41d4-a716-446655440000");
682        assert!(result.is_ok());
683    }
684
685    #[test]
686    fn test_uuid_validator_invalid() {
687        let validator = UuidIdValidator;
688        let result = validator.validate("not-a-uuid");
689        assert!(result.is_err());
690        let err = result.unwrap_err();
691        assert_eq!(err.value, "not-a-uuid");
692    }
693
694    #[test]
695    fn test_uuid_validator_format_name() {
696        let validator = UuidIdValidator;
697        assert_eq!(validator.format_name(), "UUID");
698    }
699
700    #[test]
701    fn test_uuid_validator_nil_uuid() {
702        let validator = UuidIdValidator;
703        assert!(validator.validate("00000000-0000-0000-0000-000000000000").is_ok());
704    }
705
706    #[test]
707    fn test_uuid_validator_uppercase() {
708        let validator = UuidIdValidator;
709        assert!(validator.validate("550E8400-E29B-41D4-A716-446655440000").is_ok());
710    }
711
712    // ==================== Numeric Validator Tests ====================
713
714    #[test]
715    fn test_numeric_validator_valid_positive() {
716        let validator = NumericIdValidator;
717        assert!(validator.validate("12345").is_ok());
718        assert!(validator.validate("0").is_ok());
719        assert!(validator.validate("9223372036854775807").is_ok()); // i64::MAX
720    }
721
722    #[test]
723    fn test_numeric_validator_valid_negative() {
724        let validator = NumericIdValidator;
725        assert!(validator.validate("-1").is_ok());
726        assert!(validator.validate("-12345").is_ok());
727        assert!(validator.validate("-9223372036854775808").is_ok()); // i64::MIN
728    }
729
730    #[test]
731    fn test_numeric_validator_invalid_float() {
732        let validator = NumericIdValidator;
733        let result = validator.validate("123.45");
734        assert!(result.is_err());
735        let err = result.unwrap_err();
736        assert_eq!(err.value, "123.45");
737    }
738
739    #[test]
740    fn test_numeric_validator_invalid_non_numeric() {
741        let validator = NumericIdValidator;
742        let result = validator.validate("abc123");
743        assert!(result.is_err());
744    }
745
746    #[test]
747    fn test_numeric_validator_overflow() {
748        let validator = NumericIdValidator;
749        // Too large for i64
750        let result = validator.validate("9223372036854775808");
751        assert!(result.is_err());
752    }
753
754    #[test]
755    fn test_numeric_validator_empty_string() {
756        let validator = NumericIdValidator;
757        let result = validator.validate("");
758        assert!(result.is_err());
759    }
760
761    #[test]
762    fn test_numeric_validator_format_name() {
763        let validator = NumericIdValidator;
764        assert_eq!(validator.format_name(), "integer");
765    }
766
767    // ==================== ULID Validator Tests ====================
768
769    #[test]
770    fn test_ulid_validator_valid() {
771        let validator = UlidIdValidator;
772        // Valid ULID: 01ARZ3NDEKTSV4RRFFQ69G5FAV
773        assert!(validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAV").is_ok());
774    }
775
776    #[test]
777    fn test_ulid_validator_valid_all_digits() {
778        let validator = UlidIdValidator;
779        // Valid ULID with all digits: 01234567890123456789012345
780        assert!(validator.validate("01234567890123456789012345").is_ok());
781    }
782
783    #[test]
784    fn test_ulid_validator_valid_all_uppercase() {
785        let validator = UlidIdValidator;
786        // Valid ULID with all uppercase (no I, L, O, U)
787        assert!(validator.validate("ABCDEFGHJKMNPQRSTVWXYZ0123").is_ok());
788    }
789
790    #[test]
791    fn test_ulid_validator_invalid_length_short() {
792        let validator = UlidIdValidator;
793        let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5F");
794        assert!(result.is_err());
795        let err = result.unwrap_err();
796        assert!(err.message.contains("26 characters"));
797    }
798
799    #[test]
800    fn test_ulid_validator_invalid_length_long() {
801        let validator = UlidIdValidator;
802        let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAVA");
803        assert!(result.is_err());
804        let err = result.unwrap_err();
805        assert!(err.message.contains("26 characters"));
806    }
807
808    #[test]
809    fn test_ulid_validator_invalid_lowercase() {
810        let validator = UlidIdValidator;
811        let result = validator.validate("01arz3ndektsv4rrffq69g5fav");
812        assert!(result.is_err());
813    }
814
815    #[test]
816    fn test_ulid_validator_invalid_char_i() {
817        let validator = UlidIdValidator;
818        let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAI");
819        assert!(result.is_err());
820        let err = result.unwrap_err();
821        assert!(err.message.contains("Crockford base32"));
822    }
823
824    #[test]
825    fn test_ulid_validator_invalid_char_l() {
826        let validator = UlidIdValidator;
827        let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAL");
828        assert!(result.is_err());
829    }
830
831    #[test]
832    fn test_ulid_validator_invalid_char_o() {
833        let validator = UlidIdValidator;
834        let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAO");
835        assert!(result.is_err());
836    }
837
838    #[test]
839    fn test_ulid_validator_invalid_char_u() {
840        let validator = UlidIdValidator;
841        let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAU");
842        assert!(result.is_err());
843    }
844
845    #[test]
846    fn test_ulid_validator_invalid_special_chars() {
847        let validator = UlidIdValidator;
848        let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FA-");
849        assert!(result.is_err());
850    }
851
852    #[test]
853    fn test_ulid_validator_empty_string() {
854        let validator = UlidIdValidator;
855        let result = validator.validate("");
856        assert!(result.is_err());
857    }
858
859    #[test]
860    fn test_ulid_validator_format_name() {
861        let validator = UlidIdValidator;
862        assert_eq!(validator.format_name(), "ULID");
863    }
864
865    // ==================== Opaque Validator Tests ====================
866
867    #[test]
868    fn test_opaque_validator_any_string() {
869        let validator = OpaqueIdValidator;
870        assert!(validator.validate("anything").is_ok());
871        assert!(validator.validate("12345").is_ok());
872        assert!(validator.validate("special@chars!#$%").is_ok());
873        assert!(validator.validate("").is_ok());
874    }
875
876    #[test]
877    fn test_opaque_validator_malicious_strings() {
878        let validator = OpaqueIdValidator;
879        // Opaque validator accepts anything - security is delegated to application layer
880        assert!(validator.validate("'; DROP TABLE users; --").is_ok());
881        assert!(validator.validate("../../etc/passwd").is_ok());
882        assert!(validator.validate("<script>alert('xss')</script>").is_ok());
883    }
884
885    #[test]
886    fn test_opaque_validator_uuid() {
887        let validator = OpaqueIdValidator;
888        assert!(validator.validate("550e8400-e29b-41d4-a716-446655440000").is_ok());
889    }
890
891    #[test]
892    fn test_opaque_validator_format_name() {
893        let validator = OpaqueIdValidator;
894        assert_eq!(validator.format_name(), "opaque");
895    }
896
897    // ==================== Cross-Validator Tests ====================
898
899    #[test]
900    fn test_validators_trait_object() {
901        let validators: Vec<Box<dyn IdValidator>> = vec![
902            Box::new(UuidIdValidator),
903            Box::new(NumericIdValidator),
904            Box::new(UlidIdValidator),
905            Box::new(OpaqueIdValidator),
906        ];
907
908        for validator in validators {
909            // All validators should have format names
910            let name = validator.format_name();
911            assert!(!name.is_empty());
912        }
913    }
914
915    #[test]
916    fn test_validator_selection_by_id_format() {
917        // Demonstrate using correct validator for different ID formats
918        let uuid = "550e8400-e29b-41d4-a716-446655440000";
919        let numeric = "12345";
920        let ulid = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
921
922        let uuid_validator = UuidIdValidator;
923        let numeric_validator = NumericIdValidator;
924        let ulid_validator = UlidIdValidator;
925
926        assert!(uuid_validator.validate(uuid).is_ok());
927        assert!(numeric_validator.validate(numeric).is_ok());
928        assert!(ulid_validator.validate(ulid).is_ok());
929
930        // Wrong validators should fail
931        assert!(uuid_validator.validate(numeric).is_err());
932        assert!(numeric_validator.validate(uuid).is_err());
933        assert!(ulid_validator.validate(numeric).is_err());
934    }
935
936    // ==================== ID Validation Profile Tests ====================
937
938    #[test]
939    fn test_id_validation_profile_uuid() {
940        let profile = IDValidationProfile::uuid();
941        assert_eq!(profile.name, "uuid");
942        assert!(profile.validate("550e8400-e29b-41d4-a716-446655440000").is_ok());
943        assert!(profile.validate("not-a-uuid").is_err());
944    }
945
946    #[test]
947    fn test_id_validation_profile_numeric() {
948        let profile = IDValidationProfile::numeric();
949        assert_eq!(profile.name, "numeric");
950        assert!(profile.validate("12345").is_ok());
951        assert!(profile.validate("not-a-number").is_err());
952    }
953
954    #[test]
955    fn test_id_validation_profile_ulid() {
956        let profile = IDValidationProfile::ulid();
957        assert_eq!(profile.name, "ulid");
958        assert!(profile.validate("01ARZ3NDEKTSV4RRFFQ69G5FAV").is_ok());
959        assert!(profile.validate("not-a-ulid").is_err());
960    }
961
962    #[test]
963    fn test_id_validation_profile_opaque() {
964        let profile = IDValidationProfile::opaque();
965        assert_eq!(profile.name, "opaque");
966        assert!(profile.validate("anything").is_ok());
967        assert!(profile.validate("12345").is_ok());
968        assert!(profile.validate("special@chars!#$%").is_ok());
969    }
970
971    #[test]
972    fn test_id_validation_profile_by_name() {
973        // Test exact matches
974        assert!(IDValidationProfile::by_name("uuid").is_some());
975        assert!(IDValidationProfile::by_name("numeric").is_some());
976        assert!(IDValidationProfile::by_name("ulid").is_some());
977        assert!(IDValidationProfile::by_name("opaque").is_some());
978
979        // Test case insensitivity
980        assert!(IDValidationProfile::by_name("UUID").is_some());
981        assert!(IDValidationProfile::by_name("NUMERIC").is_some());
982        assert!(IDValidationProfile::by_name("ULID").is_some());
983
984        // Test aliases
985        assert!(IDValidationProfile::by_name("integer").is_some());
986        assert!(IDValidationProfile::by_name("string").is_some());
987
988        // Test invalid
989        assert!(IDValidationProfile::by_name("invalid").is_none());
990    }
991
992    #[test]
993    fn test_id_validation_profile_by_name_uuid_validation() {
994        let profile = IDValidationProfile::by_name("uuid").unwrap();
995        assert_eq!(profile.name, "uuid");
996        assert!(profile.validate("550e8400-e29b-41d4-a716-446655440000").is_ok());
997    }
998
999    #[test]
1000    fn test_id_validation_profile_by_name_numeric_validation() {
1001        let profile = IDValidationProfile::by_name("numeric").unwrap();
1002        assert_eq!(profile.name, "numeric");
1003        assert!(profile.validate("12345").is_ok());
1004    }
1005
1006    #[test]
1007    fn test_id_validation_profile_by_name_integer_alias() {
1008        let profile_numeric = IDValidationProfile::by_name("numeric").unwrap();
1009        let profile_integer = IDValidationProfile::by_name("integer").unwrap();
1010
1011        // Both should validate the same way
1012        assert!(profile_numeric.validate("12345").is_ok());
1013        assert!(profile_integer.validate("12345").is_ok());
1014        assert!(profile_numeric.validate("not-a-number").is_err());
1015        assert!(profile_integer.validate("not-a-number").is_err());
1016    }
1017
1018    #[test]
1019    fn test_id_validation_profile_by_name_string_alias() {
1020        let profile_opaque = IDValidationProfile::by_name("opaque").unwrap();
1021        let profile_string = IDValidationProfile::by_name("string").unwrap();
1022
1023        // Both should validate the same way
1024        assert!(profile_opaque.validate("anything").is_ok());
1025        assert!(profile_string.validate("anything").is_ok());
1026    }
1027
1028    #[test]
1029    fn test_validation_profile_type_as_validator() {
1030        let uuid_type = ValidationProfileType::Uuid(UuidIdValidator);
1031        assert!(
1032            uuid_type
1033                .as_validator()
1034                .validate("550e8400-e29b-41d4-a716-446655440000")
1035                .is_ok()
1036        );
1037
1038        let numeric_type = ValidationProfileType::Numeric(NumericIdValidator);
1039        assert!(numeric_type.as_validator().validate("12345").is_ok());
1040
1041        let ulid_type = ValidationProfileType::Ulid(UlidIdValidator);
1042        assert!(ulid_type.as_validator().validate("01ARZ3NDEKTSV4RRFFQ69G5FAV").is_ok());
1043
1044        let opaque_type = ValidationProfileType::Opaque(OpaqueIdValidator);
1045        assert!(opaque_type.as_validator().validate("any_value").is_ok());
1046    }
1047
1048    #[test]
1049    fn test_id_validation_profile_clone() {
1050        let profile1 = IDValidationProfile::uuid();
1051        let profile2 = profile1.clone();
1052
1053        assert_eq!(profile1.name, profile2.name);
1054        assert!(profile1.validate("550e8400-e29b-41d4-a716-446655440000").is_ok());
1055        assert!(profile2.validate("550e8400-e29b-41d4-a716-446655440000").is_ok());
1056    }
1057
1058    #[test]
1059    fn test_all_profiles_available() {
1060        let profiles = [
1061            IDValidationProfile::uuid(),
1062            IDValidationProfile::numeric(),
1063            IDValidationProfile::ulid(),
1064            IDValidationProfile::opaque(),
1065        ];
1066
1067        assert_eq!(profiles.len(), 4);
1068        assert_eq!(profiles[0].name, "uuid");
1069        assert_eq!(profiles[1].name, "numeric");
1070        assert_eq!(profiles[2].name, "ulid");
1071        assert_eq!(profiles[3].name, "opaque");
1072    }
1073}