merge: maestro/tsk_GXDoc3Cs07Rn [tsk_nI2T8qpcoier] apierr + idgen + CONVENTIONS.md

Apply changes from tsk_GXDoc3Cs07Rn (conflict rescue: original auto-merge
blocked by uncommitted changes on main; worktree was clean, no code conflicts).

Changes applied verbatim from b64c002:
- server/CONVENTIONS.md: new file — mandatory coding conventions
- server/internal/apierr: add New(), StatusFor(), ErrUnauthorized/Forbidden/
  NotFound/Conflict predefined errors, chi Middleware; add apierr_test.go (8 tests)
- server/internal/idgen: implement idgen.go (UUID v7 + Crockford Base32 moved
  from codes); add idgen_test.go (12 tests)
- server/internal/codes/generator.go: refactor to delegate to idgen.*
- server/go.mod: move google/uuid from indirect to direct dependency

All tests pass: go test ./internal/apierr/... ./internal/idgen/... ./internal/codes/...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 14:45:46 +08:00
parent 787151245e
commit 25e0204471
9 changed files with 1064 additions and 178 deletions
+244
View File
@@ -0,0 +1,244 @@
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 (014) 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)
}
}