787151245e
解决与 1A 骨架的冲突:module 统一为 github.com/wangjia/pangolin/server (codes 分支原用 pangolin/server,7 个源文件 import 已改写); go.mod require 并集(redis 取 9.20.1);Makefile 以骨架为基底并入 build-codegen/test-unit/test-integration target。 go build/vet 通过,codes 单测通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
200 lines
5.8 KiB
Go
200 lines
5.8 KiB
Go
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)
|
||
}
|
||
}
|