test(backend): 补 redeem_code 单测 + gitignore todo/ 与 gencode 产物
覆盖 NormalizeCode 边界/幂等、GenerateRedeemCode 格式/无歧义字母表 (剔除 0/O/1/I/L)/无碰撞。.gitignore 忽略全局 todo skill 本地数据 与 backend/gencode 构建产物。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
This commit is contained in:
@@ -55,6 +55,10 @@ coverage/
|
|||||||
|
|
||||||
# 编译产物(后端可执行文件)
|
# 编译产物(后端可执行文件)
|
||||||
backend/issue
|
backend/issue
|
||||||
|
backend/gencode
|
||||||
|
|
||||||
|
# 全局 todo skill 的本地数据(看板/任务,不入库)
|
||||||
|
/todo/
|
||||||
|
|
||||||
# Claude 私有文件
|
# Claude 私有文件
|
||||||
.claude/commands/
|
.claude/commands/
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeCode(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"展示格式带连字符", "JIUKU-YD9S-7CAP", "JIUKUYD9S7CAP"},
|
||||||
|
{"小写转大写", "jiuku-yd9s-7cap", "JIUKUYD9S7CAP"},
|
||||||
|
{"去除空格", "JIUKU YD9S 7CAP", "JIUKUYD9S7CAP"},
|
||||||
|
{"去除制表/换行/回车", "JIUKU\tYD9S\n7CAP\r", "JIUKUYD9S7CAP"},
|
||||||
|
{"已归一化幂等", "JIUKUYD9S7CAP", "JIUKUYD9S7CAP"},
|
||||||
|
{"空串", "", ""},
|
||||||
|
{"混合大小写与分隔", " jiUKu--yd9s 7caP\n", "JIUKUYD9S7CAP"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
if got := NormalizeCode(c.in); got != c.want {
|
||||||
|
t.Fatalf("NormalizeCode(%q) = %q, want %q", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeCodeIdempotent(t *testing.T) {
|
||||||
|
// 二次归一化结果不变
|
||||||
|
once := NormalizeCode("jiuku-yd9s-7cap")
|
||||||
|
if twice := NormalizeCode(once); twice != once {
|
||||||
|
t.Fatalf("NormalizeCode 非幂等: %q -> %q", once, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateRedeemCodeFormat(t *testing.T) {
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
code := GenerateRedeemCode()
|
||||||
|
|
||||||
|
// 展示格式:JIUKU-XXXX-XXXX
|
||||||
|
parts := strings.Split(code, "-")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
t.Fatalf("码段数应为 3,得到 %d:%q", len(parts), code)
|
||||||
|
}
|
||||||
|
if parts[0] != "JIUKU" {
|
||||||
|
t.Fatalf("前缀应为 JIUKU,得到 %q:%q", parts[0], code)
|
||||||
|
}
|
||||||
|
if len(parts[1]) != 4 || len(parts[2]) != 4 {
|
||||||
|
t.Fatalf("随机段长度应各为 4,得到 %d/%d:%q", len(parts[1]), len(parts[2]), code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归一化后 = 前缀 + 8 位随机
|
||||||
|
norm := NormalizeCode(code)
|
||||||
|
if len(norm) != len("JIUKU")+8 {
|
||||||
|
t.Fatalf("归一化长度应为 %d,得到 %d:%q", len("JIUKU")+8, len(norm), norm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机段只含无歧义字母表字符(不含 0/O/1/I/L)
|
||||||
|
random := parts[1] + parts[2]
|
||||||
|
for _, r := range random {
|
||||||
|
if !strings.ContainsRune(codeAlphabet, r) {
|
||||||
|
t.Fatalf("出现字母表外字符 %q:%q", r, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodeAlphabetExcludesAmbiguous(t *testing.T) {
|
||||||
|
for _, bad := range []rune{'0', 'O', '1', 'I', 'L'} {
|
||||||
|
if strings.ContainsRune(codeAlphabet, bad) {
|
||||||
|
t.Fatalf("字母表不应包含易混字符 %q", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateRedeemCodeUnique(t *testing.T) {
|
||||||
|
// 不要求绝对唯一,但大量生成应几乎无碰撞(31^8 空间)
|
||||||
|
const n = 1000
|
||||||
|
seen := make(map[string]struct{}, n)
|
||||||
|
dup := 0
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
c := NormalizeCode(GenerateRedeemCode())
|
||||||
|
if _, ok := seen[c]; ok {
|
||||||
|
dup++
|
||||||
|
}
|
||||||
|
seen[c] = struct{}{}
|
||||||
|
}
|
||||||
|
if dup > 0 {
|
||||||
|
t.Fatalf("%d 次生成出现 %d 次重复,随机性可疑", n, dup)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user