package codes_test import ( "strings" "testing" "github.com/wangjia/pangolin/server/internal/codes" ) // TestCrockfordAlphabetCoverage verifies the 32-symbol encoding alphabet. func TestCrockfordAlphabetCoverage(t *testing.T) { forbidden := []rune{'I', 'L', 'O', 'U'} alpha := "0123456789ABCDEFGHJKMNPQRSTVWXYZ" if len(alpha) != 32 { t.Fatalf("alphabet length = %d, want 32", len(alpha)) } for _, ch := range forbidden { if strings.ContainsRune(alpha, ch) { t.Errorf("forbidden char %c found in alphabet", ch) } } } // TestGenerateCodeFormat verifies that GenerateCode returns a 16-char code // made entirely of the Crockford alphabet (first 15 chars) + valid check char. func TestGenerateCodeFormat(t *testing.T) { for i := 0; i < 1000; i++ { code, err := codes.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)) } // Canonicalize must accept a freshly generated code. canonical, err := codes.Canonicalize(code) if err != nil { t.Errorf("Canonicalize(%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 := codes.GenerateCode() if err != nil { t.Fatalf("GenerateCode: %v", err) } h := codes.Hash(code) if _, dup := seen[h]; dup { t.Fatalf("hash collision at iteration %d: code=%s hash=%s", i, code, h) } seen[h] = struct{}{} } } // TestCanonicalizeNormalization verifies the I/L→1 and O→0 substitutions. func TestCanonicalizeNormalization(t *testing.T) { // Generate a valid code to use as a base. base, err := codes.GenerateCode() if err != nil { t.Fatalf("GenerateCode: %v", err) } // Test lower-case input equals upper-case canonical. lower := strings.ToLower(base) canonical, err := codes.Canonicalize(lower) if err != nil { t.Errorf("Canonicalize(lower) error: %v", err) } if canonical != base { t.Errorf("Canonicalize(lower) = %q, want %q", canonical, base) } // Test substitution: replace a '1' in the code with 'I' and 'L', verify they // normalise to the same canonical form. idx := strings.IndexByte(base, '1') if idx >= 0 && idx < 15 { withI := base[:idx] + "I" + base[idx+1:] withL := base[:idx] + "L" + base[idx+1:] withLower := base[:idx] + "l" + base[idx+1:] for _, variant := range []string{withI, withL, withLower} { c, err := codes.Canonicalize(variant) if err != nil { t.Errorf("Canonicalize(%q) error: %v", variant, err) continue } if c != base { t.Errorf("Canonicalize(%q) = %q, want %q", variant, c, base) } } } // Replace a '0' with 'O' and verify normalisation. idx = strings.IndexByte(base, '0') if idx >= 0 && idx < 15 { withO := base[:idx] + "O" + base[idx+1:] c, err := codes.Canonicalize(withO) if err != nil { t.Errorf("Canonicalize(%q) error: %v", withO, err) } else if c != base { t.Errorf("Canonicalize(%q) = %q, want %q", withO, c, base) } } } // TestCheckCharDetectsSingleErrors verifies that mutating any single data // character in a valid code causes Canonicalize to return an error. func TestCheckCharDetectsSingleErrors(t *testing.T) { const alpha = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" code, err := codes.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:] _, err := codes.Canonicalize(mutated) if err == nil { t.Errorf("mutating pos %d (%c→%c) not detected: code=%q mutated=%q", pos, original, replacement, code, mutated) } } } } // TestHashConsistency verifies that Hash is deterministic and that different // codes produce different hashes. func TestHashConsistency(t *testing.T) { code1, _ := codes.GenerateCode() code2, _ := codes.GenerateCode() for code1 == code2 { code2, _ = codes.GenerateCode() } h1a := codes.Hash(code1) h1b := codes.Hash(code1) h2 := codes.Hash(code2) if h1a != h1b { t.Error("Hash is not deterministic") } if h1a == h2 { t.Error("Different codes produced the same hash") } if len(h1a) != 64 { t.Errorf("Hash length = %d, want 64 (hex SHA-256)", len(h1a)) } } // TestCanonicalizeRejectsInvalidLength tests length validation. func TestCanonicalizeRejectsInvalidLength(t *testing.T) { cases := []string{"", "ABCDE", "ABCDEFGH12345678X"} for _, c := range cases { if _, err := codes.Canonicalize(c); err == nil { t.Errorf("Canonicalize(%q) should fail but did not", c) } } } // TestCanonicalizeRejectsInvalidChars tests that characters outside the // Crockford alphabet are rejected for data positions. func TestCanonicalizeRejectsInvalidChars(t *testing.T) { base, _ := codes.GenerateCode() // Replace position 0 with an invalid character. invalid := "!" + base[1:] if _, err := codes.Canonicalize(invalid); err == nil { t.Errorf("Canonicalize(%q) should fail for invalid char", invalid) } } // TestHyphenStripping verifies that hyphens inserted for readability are stripped. func TestHyphenStripping(t *testing.T) { code, err := codes.GenerateCode() if err != nil { t.Fatalf("GenerateCode: %v", err) } // Insert hyphens: XXXX-XXXX-XXXX-XXXX hyphenated := code[:4] + "-" + code[4:8] + "-" + code[8:12] + "-" + code[12:] canonical, err := codes.Canonicalize(hyphenated) if err != nil { t.Errorf("Canonicalize(hyphenated) error: %v", err) } if canonical != code { t.Errorf("Canonicalize(hyphenated) = %q, want %q", canonical, code) } }