Files
pangolin/server/internal/codes/generator_test.go
T
wangjia 787151245e merge: maestro/tsk_tFMU7-hKzfOf [tsk_tFMU7-hKzfOf] codes 激活码模块
解决与 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>
2026-06-13 03:03:11 +08:00

200 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}