package idgen_test import ( "strings" "testing" "github.com/google/uuid" "github.com/wangjia/pangolin/server/internal/idgen" ) // ───────────────────────────────────────────────────────────────────────────── // UUID v7 tests // ───────────────────────────────────────────────────────────────────────────── // TestNewReturnsVersion7 verifies that New() returns a UUID with version 7. func TestNewReturnsVersion7(t *testing.T) { for i := 0; i < 100; i++ { id := idgen.New() if id.Version() != uuid.Version(7) { t.Fatalf("New(): version = %d, want 7", id.Version()) } } } // TestNewStringFormat verifies that NewString() returns a properly formatted UUID string. func TestNewStringFormat(t *testing.T) { s := idgen.NewString() // Standard UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 chars) if len(s) != 36 { t.Fatalf("NewString() length = %d, want 36", len(s)) } parts := strings.Split(s, "-") if len(parts) != 5 { t.Fatalf("NewString() has %d hyphen-separated parts, want 5", len(parts)) } expected := []int{8, 4, 4, 4, 12} for i, p := range parts { if len(p) != expected[i] { t.Errorf("part %d: length = %d, want %d", i, len(p), expected[i]) } } } // TestNewUniqueness verifies that 10 000 generated UUIDs are all distinct. func TestNewUniqueness(t *testing.T) { const n = 10_000 seen := make(map[string]struct{}, n) for i := 0; i < n; i++ { s := idgen.NewString() if _, dup := seen[s]; dup { t.Fatalf("duplicate UUID at iteration %d: %s", i, s) } seen[s] = struct{}{} } } // TestNewTimeOrdered verifies that sequentially generated UUIDs are // monotonically non-decreasing in their string representation (UUID v7 is // time-ordered so lexicographic sort ≈ generation order). func TestNewTimeOrdered(t *testing.T) { prev := idgen.NewString() for i := 0; i < 1000; i++ { next := idgen.NewString() if next < prev { t.Fatalf("UUID ordering violation: %s > %s", prev, next) } prev = next } } // ───────────────────────────────────────────────────────────────────────────── // Crockford Base32 activation-code tests // ───────────────────────────────────────────────────────────────────────────── // TestGenerateCodeFormat verifies that GenerateCode returns a 16-char code // composed entirely of the Crockford alphabet (15 data chars + 1 check char). func TestGenerateCodeFormat(t *testing.T) { for i := 0; i < 1000; i++ { code, err := idgen.GenerateCode() if err != nil { t.Fatalf("GenerateCode error: %v", err) } if len(code) != 16 { t.Errorf("code %q: length = %d, want 16", code, len(code)) } // A freshly generated code must canonicalize to itself. canonical, err := idgen.CanonicalizeCode(code) if err != nil { t.Errorf("CanonicalizeCode(%q): %v", code, err) } if canonical != code { t.Errorf("canonical form mismatch: got %q, want %q", canonical, code) } } } // TestGenerateCodeUniqueness checks that 10 000 generated codes have no // hash collisions (birthday probability ≈ 10^−8 for 75-bit codes). func TestGenerateCodeUniqueness(t *testing.T) { const n = 10_000 seen := make(map[string]struct{}, n) for i := 0; i < n; i++ { code, err := idgen.GenerateCode() if err != nil { t.Fatalf("GenerateCode: %v", err) } h := idgen.HashCode(code) if _, dup := seen[h]; dup { t.Fatalf("hash collision at iteration %d: code=%s hash=%s", i, code, h) } seen[h] = struct{}{} } } // TestCanonicalizeCodeNormalization verifies I/L→1 and O→0 substitutions. func TestCanonicalizeCodeNormalization(t *testing.T) { base, err := idgen.GenerateCode() if err != nil { t.Fatalf("GenerateCode: %v", err) } // Lower-case input must produce the upper-case canonical form. lower := strings.ToLower(base) canonical, err := idgen.CanonicalizeCode(lower) if err != nil { t.Errorf("CanonicalizeCode(lower) error: %v", err) } if canonical != base { t.Errorf("CanonicalizeCode(lower) = %q, want %q", canonical, base) } // I, L → 1. idx := strings.IndexByte(base, '1') if idx >= 0 && idx < 15 { for _, sub := range []string{"I", "L", "i", "l"} { variant := base[:idx] + sub + base[idx+1:] c, err := idgen.CanonicalizeCode(variant) if err != nil { t.Errorf("CanonicalizeCode(%q) error: %v", variant, err) continue } if c != base { t.Errorf("CanonicalizeCode(%q) = %q, want %q", variant, c, base) } } } // O → 0. idx = strings.IndexByte(base, '0') if idx >= 0 && idx < 15 { variant := base[:idx] + "O" + base[idx+1:] c, err := idgen.CanonicalizeCode(variant) if err != nil { t.Errorf("CanonicalizeCode(%q) error: %v", variant, err) } else if c != base { t.Errorf("CanonicalizeCode(%q) = %q, want %q", variant, c, base) } } } // TestCheckCharDetectsSingleErrors verifies that mutating any single data // character in a valid code causes CanonicalizeCode to return an error. func TestCheckCharDetectsSingleErrors(t *testing.T) { const alpha = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" code, err := idgen.GenerateCode() if err != nil { t.Fatalf("GenerateCode: %v", err) } for pos := 0; pos < 15; pos++ { original := rune(code[pos]) for _, replacement := range alpha { if replacement == original { continue } mutated := code[:pos] + string(replacement) + code[pos+1:] if _, err := idgen.CanonicalizeCode(mutated); err == nil { t.Errorf("mutating pos %d (%c→%c) not detected: code=%q mutated=%q", pos, original, replacement, code, mutated) } } } } // TestHashCodeConsistency verifies that HashCode is deterministic and that // different codes produce different digests. func TestHashCodeConsistency(t *testing.T) { code1, _ := idgen.GenerateCode() code2, _ := idgen.GenerateCode() for code1 == code2 { code2, _ = idgen.GenerateCode() } h1a := idgen.HashCode(code1) h1b := idgen.HashCode(code1) h2 := idgen.HashCode(code2) if h1a != h1b { t.Error("HashCode is not deterministic") } if h1a == h2 { t.Error("different codes produced the same digest") } if len(h1a) != 64 { t.Errorf("HashCode length = %d, want 64 (hex SHA-256)", len(h1a)) } } // TestCanonicalizeCodeRejectsInvalidLength tests length validation. func TestCanonicalizeCodeRejectsInvalidLength(t *testing.T) { cases := []string{"", "ABCDE", "ABCDEFGH12345678X"} for _, c := range cases { if _, err := idgen.CanonicalizeCode(c); err == nil { t.Errorf("CanonicalizeCode(%q) should fail for length %d", c, len(c)) } } } // TestCanonicalizeCodeRejectsInvalidChars tests that non-Crockford characters // at data positions (0–14) are rejected. func TestCanonicalizeCodeRejectsInvalidChars(t *testing.T) { base, _ := idgen.GenerateCode() invalid := "!" + base[1:] if _, err := idgen.CanonicalizeCode(invalid); err == nil { t.Errorf("CanonicalizeCode(%q) should fail for invalid character", invalid) } } // TestHyphenStripping verifies that hyphens inserted for display readability // (e.g. XXXX-XXXX-XXXX-XXXX) are stripped before validation. func TestHyphenStripping(t *testing.T) { code, err := idgen.GenerateCode() if err != nil { t.Fatalf("GenerateCode: %v", err) } hyphenated := code[:4] + "-" + code[4:8] + "-" + code[8:12] + "-" + code[12:] canonical, err := idgen.CanonicalizeCode(hyphenated) if err != nil { t.Errorf("CanonicalizeCode(hyphenated) error: %v", err) } if canonical != code { t.Errorf("CanonicalizeCode(hyphenated) = %q, want %q", canonical, code) } }