Files
encryptx/utilsx/utilsx_test.go
T

728 lines
22 KiB
Go
Raw Normal View History

2026-09-30 15:49:12 +08:00
package utilsx_test
2025-11-03 23:40:41 +08:00
import (
2026-09-30 15:49:12 +08:00
"bytes"
"crypto/rand"
2025-11-03 23:40:41 +08:00
"encoding/base64"
"encoding/hex"
"strings"
"testing"
2026-09-30 15:49:12 +08:00
"code.yun.ink/pkg/encryptx/utilsx"
2025-11-03 23:40:41 +08:00
)
2026-09-30 15:49:12 +08:00
// urlSafeAlphabet 是 URL 安全 Base64(无填充)允许出现的全部字符。
const urlSafeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
2025-11-03 23:40:41 +08:00
2026-09-30 15:49:12 +08:00
// TestRandomBytes 校验长度、随机性与错误路径。
func TestRandomBytes(t *testing.T) {
for _, n := range []int{1, 2, 16, 32, 1024} {
buf, err := utilsx.RandomBytes(n)
if err != nil {
t.Fatalf("RandomBytes(%d) 返回错误: %v", n, err)
}
if len(buf) != n {
t.Errorf("RandomBytes(%d) 长度 = %d", n, len(buf))
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// 错误路径:0、负数、超大值都必须返回 error 与 nil,且不 panic。
bad := []int{0, -1, -1024, (1 << 20) + 1, 1 << 30}
for _, n := range bad {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("RandomBytes(%d) 发生 panic: %v", n, r)
}
}()
buf, err := utilsx.RandomBytes(n)
if err == nil {
t.Errorf("RandomBytes(%d) 应返回错误", n)
}
if buf != nil {
t.Errorf("RandomBytes(%d) 出错时应返回 nil", n)
}
}()
}
// 边界值 1<<20 必须被接受。
if buf, err := utilsx.RandomBytes(1 << 20); err != nil || len(buf) != 1<<20 {
t.Errorf("RandomBytes(1<<20) 应成功,err=%v len=%d", err, len(buf))
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestRandomBytesUnique 校验重复调用产生不同结果。
func TestRandomBytesUnique(t *testing.T) {
assertUnique(t, 64, func() string {
buf, err := utilsx.RandomBytes(32)
if err != nil {
t.Fatalf("RandomBytes 返回错误: %v", err)
}
return string(buf)
})
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
// TestRandomString 校验字符集、长度、随机性与错误路径。
func TestRandomString(t *testing.T) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
s, err := utilsx.RandomString(32)
2025-11-03 23:40:41 +08:00
if err != nil {
2026-09-30 15:49:12 +08:00
t.Fatalf("RandomString 返回错误: %v", err)
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
if len(s) != 32 {
t.Errorf("RandomString(32) 长度 = %d", len(s))
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
for i := 0; i < len(s); i++ {
if strings.IndexByte(charset, s[i]) < 0 {
t.Errorf("RandomString 含非法字符 %q", s[i])
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
assertUnique(t, 64, func() string {
v, err := utilsx.RandomString(24)
if err != nil {
t.Fatalf("RandomString 返回错误: %v", err)
}
return v
})
2025-11-03 23:40:41 +08:00
2026-09-30 15:49:12 +08:00
for _, n := range []int{0, -5, (1 << 20) + 1} {
v, err := utilsx.RandomString(n)
if err == nil {
t.Errorf("RandomString(%d) 应返回错误", n)
}
if v != "" {
t.Errorf("RandomString(%d) 出错时应返回空串,实际 %q", n, v)
}
}
}
// TestRandomStringWithCharset 校验自定义字符集与错误路径。
func TestRandomStringWithCharset(t *testing.T) {
s, err := utilsx.RandomStringWithCharset(64, "ab")
if err != nil {
t.Fatalf("RandomStringWithCharset 返回错误: %v", err)
}
if len(s) != 64 {
t.Errorf("长度 = %d, 期望 64", len(s))
}
for i := 0; i < len(s); i++ {
if s[i] != 'a' && s[i] != 'b' {
t.Errorf("字符集受限却出现了 %q", s[i])
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// 单字符字符集应始终返回同内容但长度正确的串。
single, err := utilsx.RandomStringWithCharset(8, "x")
2025-11-03 23:40:41 +08:00
if err != nil {
2026-09-30 15:49:12 +08:00
t.Fatalf("单字符字符集出错: %v", err)
}
if single != "xxxxxxxx" {
t.Errorf("单字符字符集结果 = %q, 期望 xxxxxxxx", single)
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
// 空字符集必须报错。
v, err := utilsx.RandomStringWithCharset(16, "")
if err == nil {
t.Error("空字符集应返回错误")
}
if v != "" {
t.Errorf("空字符集出错时应返回空串,实际 %q", v)
}
// 非 ASCII 字符集:长度按 rune 计。
uni, err := utilsx.RandomStringWithCharset(20, "中文🔐")
if err != nil {
t.Fatalf("非 ASCII 字符集出错: %v", err)
}
if len([]rune(uni)) != 20 {
t.Errorf("非 ASCII 字符集长度 = %d rune, 期望 20", len([]rune(uni)))
}
// 长度非法。
if _, err := utilsx.RandomStringWithCharset(0, "abc"); err == nil {
t.Error("长度为 0 应返回错误")
}
if _, err := utilsx.RandomStringWithCharset(-1, "abc"); err == nil {
t.Error("长度为负应返回错误")
}
if _, err := utilsx.RandomStringWithCharset((1<<20)+1, "abc"); err == nil {
t.Error("长度超上限应返回错误")
}
}
// TestRandomHex 校验十六进制输出的长度、字符集与随机性。
func TestRandomHex(t *testing.T) {
got, err := utilsx.RandomHex(16)
if err != nil {
t.Fatalf("RandomHex 返回错误: %v", err)
}
if len(got) != 32 {
t.Errorf("RandomHex(16) 长度 = %d, 期望 32", len(got))
}
if _, err := hex.DecodeString(got); err != nil {
t.Errorf("RandomHex 结果不是合法十六进制: %v", err)
}
if got != strings.ToLower(got) {
t.Errorf("RandomHex 应为小写: %q", got)
}
assertUnique(t, 64, func() string {
v, err := utilsx.RandomHex(16)
if err != nil {
t.Fatalf("RandomHex 返回错误: %v", err)
}
return v
})
for _, n := range []int{0, -1, (1 << 20) + 1} {
v, err := utilsx.RandomHex(n)
if err == nil {
t.Errorf("RandomHex(%d) 应返回错误", n)
}
if v != "" {
t.Errorf("RandomHex(%d) 出错时应返回空串", n)
}
}
}
// TestRandomBase64 校验 URL 安全无填充 Base64 输出的长度与字符集。
func TestRandomBase64(t *testing.T) {
cases := []struct {
n int
want int // 期望的编码后长度(ceil(4n/3))
}{
{1, 2}, {2, 3}, {3, 4}, {4, 6}, {16, 22}, {32, 43}, {48, 64},
}
for _, c := range cases {
got, err := utilsx.RandomBase64(c.n)
if err != nil {
t.Fatalf("RandomBase64(%d) 返回错误: %v", c.n, err)
}
if len(got) != c.want {
t.Errorf("RandomBase64(%d) 长度 = %d, 期望 %d", c.n, len(got), c.want)
}
if strings.Contains(got, "=") {
t.Errorf("RandomBase64 不应含填充: %q", got)
}
assertOnlyCharset(t, got, urlSafeAlphabet)
if _, err := base64.RawURLEncoding.DecodeString(got); err != nil {
t.Errorf("RandomBase64(%d) 结果不是合法 RawURLEncoding: %v", c.n, err)
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
assertUnique(t, 64, func() string {
v, err := utilsx.RandomBase64(24)
if err != nil {
t.Fatalf("RandomBase64 返回错误: %v", err)
}
return v
})
2025-11-03 23:40:41 +08:00
2026-09-30 15:49:12 +08:00
for _, n := range []int{0, -3, (1 << 20) + 1} {
v, err := utilsx.RandomBase64(n)
if err == nil {
t.Errorf("RandomBase64(%d) 应返回错误", n)
}
if v != "" {
t.Errorf("RandomBase64(%d) 出错时应返回空串", n)
}
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestRandomToken 校验长度恰为 n、只含 URL 安全字符、无填充,
// 以及"当 n 是合法无填充 Base64 长度时可解回字节"。
func TestRandomToken(t *testing.T) {
// 覆盖全部 n mod 4 情形。
lengths := []int{utilsx.MinTokenLength, 17, 18, 19, 20, 21, 24, 25, 32, 33, 64, utilsx.DefaultTokenLength, 100, 128, 129}
for _, n := range lengths {
got, err := utilsx.RandomToken(n)
if err != nil {
t.Fatalf("RandomToken(%d) 返回错误: %v", n, err)
}
if len(got) != n {
t.Errorf("RandomToken(%d) 长度 = %d", n, len(got))
}
if strings.Contains(got, "=") {
t.Errorf("RandomToken(%d) 不应含填充: %q", n, got)
}
assertOnlyCharset(t, got, urlSafeAlphabet)
2025-11-03 23:40:41 +08:00
2026-09-30 15:49:12 +08:00
// n ≡ 1 (mod 4) 不是合法的无填充 Base64 长度(残留 1 字符无法还原成整字节),
// 因此这类长度下只断言字符集与长度;其余长度必须能原样解回字节。
if n%4 == 1 {
if _, err := base64.RawURLEncoding.DecodeString(got); err == nil {
t.Logf("RandomToken(%d) 意外可被 RawURLEncoding 解码(标准库行为可能已放宽)", n)
}
continue
}
raw, err := base64.RawURLEncoding.DecodeString(got)
if err != nil {
t.Errorf("RandomToken(%d)(n%%4=%d)结果不是合法 RawURLEncoding: %v", n, n%4, err)
continue
}
if wantBytes := (n * 3) / 4; len(raw) != wantBytes {
t.Errorf("RandomToken(%d) 可还原 %d 字节, 期望 %d", n, len(raw), wantBytes)
}
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
// 低于最小长度必须报错(且返回空串、不 panic)。
for _, n := range []int{0, 1, 8, utilsx.MinTokenLength - 1, -1} {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("RandomToken(%d) 发生 panic: %v", n, r)
}
}()
v, err := utilsx.RandomToken(n)
if err == nil {
t.Errorf("RandomToken(%d) 应返回错误(低于 MinTokenLength=%d)", n, utilsx.MinTokenLength)
}
if v != "" {
t.Errorf("RandomToken(%d) 出错时应返回空串", n)
}
}()
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
// 超过上限必须报错。
if _, err := utilsx.RandomToken((1 << 20) + 1); err == nil {
t.Error("RandomToken 超上限应返回错误")
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
assertUnique(t, 128, func() string {
v, err := utilsx.RandomToken(32)
if err != nil {
t.Fatalf("RandomToken 返回错误: %v", err)
}
return v
})
}
// TestRandomPasswordCategories 断言每一个被选中的字符类别至少出现一次。
// 这是本次重写要修复的核心缺陷,因此循环 50 次以确认稳定通过。
func TestRandomPasswordCategories(t *testing.T) {
const iterations = 50
combos := []struct {
upper, lower, digits, symbols bool
}{
{true, false, false, false},
{false, true, false, false},
{false, false, true, false},
{false, false, false, true},
{true, true, false, false},
{true, false, true, false},
{true, false, false, true},
{false, true, true, false},
{false, true, false, true},
{false, false, true, true},
{true, true, true, false},
{true, true, false, true},
{true, false, true, true},
{false, true, true, true},
{true, true, true, true},
}
for _, combo := range combos {
classes := 0
for _, on := range []bool{combo.upper, combo.lower, combo.digits, combo.symbols} {
if on {
classes++
}
}
length := classes + 8 // 保证有填充位置
for iter := 0; iter < iterations; iter++ {
pw, err := utilsx.RandomPassword(length, combo.upper, combo.lower, combo.digits, combo.symbols)
if err != nil {
t.Fatalf("组合 %+v 第 %d 次: RandomPassword 返回错误: %v", combo, iter, err)
}
if len(pw) != length {
t.Fatalf("组合 %+v 第 %d 次: 长度 = %d, 期望 %d", combo, iter, len(pw), length)
}
var hasUpper, hasLower, hasDigit, hasSymbol bool
for i := 0; i < len(pw); i++ {
c := pw[i]
switch {
case c >= 'A' && c <= 'Z':
hasUpper = true
case c >= 'a' && c <= 'z':
hasLower = true
case c >= '0' && c <= '9':
hasDigit = true
case strings.IndexByte("!@#$%^&*()-_=+[]{}|;:,.<>?", c) >= 0:
hasSymbol = true
default:
t.Fatalf("组合 %+v 第 %d 次: 出现未知字符 %q(口令已隐去)", combo, iter, c)
}
}
if combo.upper && !hasUpper {
t.Fatalf("组合 %+v 第 %d 次: 缺少大写字母", combo, iter)
}
if combo.lower && !hasLower {
t.Fatalf("组合 %+v 第 %d 次: 缺少小写字母", combo, iter)
}
if combo.digits && !hasDigit {
t.Fatalf("组合 %+v 第 %d 次: 缺少数字", combo, iter)
}
if combo.symbols && !hasSymbol {
t.Fatalf("组合 %+v 第 %d 次: 缺少符号", combo, iter)
}
// 未选中的类别不应出现(因为字符集只由选中类别构成)。
if !combo.upper && hasUpper {
t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的大写字母", combo, iter)
}
if !combo.lower && hasLower {
t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的小写字母", combo, iter)
}
if !combo.digits && hasDigit {
t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的数字", combo, iter)
}
if !combo.symbols && hasSymbol {
t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的符号", combo, iter)
}
}
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestRandomPasswordShuffle 校验洗牌生效:首个字符的类别不应固定。
// 若实现只做"每类先取一个再顺序拼接",则首位必然是大写字母,本测试会失败。
func TestRandomPasswordShuffle(t *testing.T) {
firstIsUpper := 0
const iterations = 200
for i := 0; i < iterations; i++ {
pw, err := utilsx.RandomPassword(16, true, true, true, true)
if err != nil {
t.Fatalf("RandomPassword 返回错误: %v", err)
}
if pw[0] >= 'A' && pw[0] <= 'Z' {
firstIsUpper++
}
2025-11-03 23:40:41 +08:00
}
2026-09-30 15:49:12 +08:00
// 若未洗牌则 firstIsUpper == iterations;4 类均匀时期望约 1/4。
if firstIsUpper == iterations {
t.Error("首位始终是大写字母,说明 Fisher-Yates 洗牌未生效")
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestRandomTokenExhaustiveLengths 穷举 n 从 MinTokenLength 到 1024,
// 断言返回值长度恰为 n、字符集合法,并且在 n 为合法无填充 Base64 长度
// (n%4 != 1)时一定可以解回字节。
func TestRandomTokenExhaustiveLengths(t *testing.T) {
const maxLen = 1024
for n := utilsx.MinTokenLength; n <= maxLen; n++ {
got, err := utilsx.RandomToken(n)
if err != nil {
t.Fatalf("RandomToken(%d) 返回错误: %v", n, err)
}
if len(got) != n {
t.Fatalf("RandomToken(%d) 长度 = %d", n, len(got))
}
assertOnlyCharset(t, got, urlSafeAlphabet)
if n%4 == 1 {
continue // 该长度在无填充 Base64 中不合法,见 RandomToken 文档
}
if _, err := base64.RawURLEncoding.DecodeString(got); err != nil {
t.Fatalf("RandomToken(%d)(n%%4=%d)结果不是合法 RawURLEncoding: %v", n, n%4, err)
}
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestRawBase64LengthConstraint 固定住"n ≡ 1 (mod 4) 不是合法无填充 Base64 长度"
// 这一事实,避免以后有人误以为 RandomToken 在该长度下可解码。
func TestRawBase64LengthConstraint(t *testing.T) {
for _, n := range []int{13, 17, 21, 25, 33, 97} {
s := strings.Repeat("A", n)
if _, err := base64.RawURLEncoding.DecodeString(s); err == nil {
t.Errorf("长度为 %d (n%%4=%d) 的字符串不应可被 RawURLEncoding 解码", n, n%4)
}
}
// 合法长度(n%4 ∈ {0,2,3})可以解码。
for _, n := range []int{16, 18, 19, 20, 22, 23, 32} {
s := strings.Repeat("A", n)
if _, err := base64.RawURLEncoding.DecodeString(s); err != nil {
t.Errorf("长度为 %d (n%%4=%d) 的字符串应可被 RawURLEncoding 解码: %v", n, n%4, err)
}
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestRandomPasswordErrors 校验类别数与长度矛盾时返回 error。
func TestRandomPasswordErrors(t *testing.T) {
cases := []struct {
name string
length int
upper, lower, digits, symbols bool
}{
{"长度 0", 0, true, true, true, true},
{"长度为负", -1, true, true, true, true},
{"长度 3 但需 4 类", 3, true, true, true, true},
{"长度 1 但需 4 类", 1, true, true, true, true},
{"长度 1 但需 2 类", 1, true, false, true, false},
{"未选择任何类别", 16, false, false, false, false},
{"未选择任何类别且长度 0", 0, false, false, false, false},
{"长度足够但无类别", 64, false, false, false, false},
{"超上限", (1 << 20) + 1, true, true, true, true},
}
for _, c := range cases {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("%s: RandomPassword 发生 panic: %v", c.name, r)
}
}()
pw, err := utilsx.RandomPassword(c.length, c.upper, c.lower, c.digits, c.symbols)
if err == nil {
t.Errorf("%s: 应返回错误,实际得到长度 %d 的口令", c.name, len(pw))
}
if pw != "" {
t.Errorf("%s: 出错时应返回空串", c.name)
}
}()
}
// 恰好等于类别数的长度必须成功。
pw, err := utilsx.RandomPassword(4, true, true, true, true)
if err != nil {
t.Fatalf("长度等于类别数应成功: %v", err)
}
if len(pw) != 4 {
t.Errorf("长度 = %d, 期望 4", len(pw))
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestUUID 校验 RFC 4122 v4 的格式、版本位、变体位与随机性。
func TestUUID(t *testing.T) {
seen := make(map[string]bool)
for i := 0; i < 500; i++ {
id, err := utilsx.UUID()
if err != nil {
t.Fatalf("UUID 返回错误: %v", err)
}
if len(id) != 36 {
t.Fatalf("UUID 长度 = %d, 期望 36(%q)", len(id), id)
}
if id[8] != '-' || id[13] != '-' || id[18] != '-' || id[23] != '-' {
t.Fatalf("UUID 连字符位置错误: %q", id)
}
if strings.Count(id, "-") != 4 {
t.Fatalf("UUID 连字符数量错误: %q", id)
}
// 第 15 个字符(索引 14)必须是版本号 4。
if id[14] != '4' {
t.Fatalf("UUID 版本位错误: 索引 14 = %q, 期望 '4'(%q)", id[14], id)
}
// 第 20 个字符(索引 19)必须属于 8/9/a/b(RFC 4122 变体 10xx)。
if !strings.ContainsRune("89ab", rune(id[19])) {
t.Fatalf("UUID 变体位错误: 索引 19 = %q, 期望 8/9/a/b(%q)", id[19], id)
}
if id != strings.ToLower(id) {
t.Fatalf("UUID 应为小写: %q", id)
}
// 去掉连字符后必须是 32 位十六进制。
if _, err := hex.DecodeString(strings.ReplaceAll(id, "-", "")); err != nil {
t.Fatalf("UUID 主体不是合法十六进制: %v(%q)", err, id)
}
if seen[id] {
t.Fatalf("UUID 出现重复: %q", id)
}
seen[id] = true
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestAPIKey 校验前缀处理与令牌长度。
func TestAPIKey(t *testing.T) {
const n = 32
key, err := utilsx.APIKey("sk.", n)
if err != nil {
t.Fatalf("APIKey 返回错误: %v", err)
}
if !strings.HasPrefix(key, "sk.") {
t.Errorf("APIKey 未包含前缀: %q", key)
}
if len(key) != len("sk.")+n {
t.Errorf("APIKey 长度 = %d, 期望 %d", len(key), len("sk.")+n)
}
assertOnlyCharset(t, strings.TrimPrefix(key, "sk."), urlSafeAlphabet)
if strings.Contains(key, "=") {
t.Errorf("APIKey 不应含填充: %q", key)
}
// 空前缀:直接返回令牌。
plain, err := utilsx.APIKey("", n)
if err != nil {
t.Fatalf("APIKey(空前缀) 返回错误: %v", err)
}
if len(plain) != n {
t.Errorf("APIKey(空前缀) 长度 = %d, 期望 %d", len(plain), n)
}
// 前缀仅用于人类可读的区分,长度校验只针对令牌部分。
if _, err := utilsx.APIKey("sk.", 0); err == nil {
t.Error("APIKey 令牌长度为 0 应返回错误")
}
if _, err := utilsx.APIKey("sk.", utilsx.MinTokenLength-1); err == nil {
t.Error("APIKey 令牌长度低于 MinTokenLength 应返回错误")
}
if v, err := utilsx.APIKey("sk.", -1); err == nil || v != "" {
t.Errorf("APIKey 负数长度应返回错误与空串,得到 %q, %v", v, err)
}
assertUnique(t, 64, func() string {
v, err := utilsx.APIKey("sk.", 32)
if err != nil {
t.Fatalf("APIKey 返回错误: %v", err)
}
return v
})
}
// TestSecureCompare 校验常量时间比较函数的行为。
func TestSecureCompare(t *testing.T) {
a := []byte("a-secret-value-to-compare")
b := append([]byte(nil), a...)
if !utilsx.SecureCompare(a, b) {
t.Error("SecureCompare 对相同内容返回 false")
}
if utilsx.SecureCompare(a, []byte("a-secret-value-to-compare-x")) {
t.Error("SecureCompare 对更长内容返回 true")
}
diff := append([]byte(nil), a...)
diff[len(diff)-1] ^= 0x01
if utilsx.SecureCompare(a, diff) {
t.Error("SecureCompare 对仅末字节不同的内容返回 true")
}
if !utilsx.SecureCompare(nil, nil) {
t.Error("SecureCompare(nil, nil) 应为 true")
}
if utilsx.SecureCompare(nil, []byte{0x00}) {
t.Error("SecureCompare(nil, 非空) 应为 false")
}
if !utilsx.SecureCompareString("token-value", "token-value") {
t.Error("SecureCompareString 对相同内容返回 false")
}
if utilsx.SecureCompareString("token-value", "token-valuE") {
t.Error("SecureCompareString 对大小写不同的内容返回 true")
}
if utilsx.SecureCompareString("", "") != true {
t.Error("SecureCompareString(\"\", \"\") 应为 true")
}
// 与 RandomToken 的实际使用场景一致:比较相等的令牌应为 true。
token, err := utilsx.RandomToken(32)
if err != nil {
t.Fatalf("RandomToken 返回错误: %v", err)
}
if !utilsx.SecureCompareString(token, token) {
t.Error("SecureCompareString 对相同令牌返回 false")
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestConstants 校验导出的长度常量为文档约定的值。
func TestConstants(t *testing.T) {
if utilsx.MinTokenLength != 16 {
t.Errorf("MinTokenLength = %d, 期望 16", utilsx.MinTokenLength)
}
if utilsx.DefaultTokenLength != 32 {
t.Errorf("DefaultTokenLength = %d, 期望 32", utilsx.DefaultTokenLength)
}
if utilsx.DefaultTokenLength < utilsx.MinTokenLength {
t.Error("DefaultTokenLength 不应小于 MinTokenLength")
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestOutputsAreNotAllSame 用固定长度检查各随机函数确实在变化(弱随机实现的兜底检查)。
func TestOutputsAreNotAllSame(t *testing.T) {
generators := map[string]func() (string, error){
"RandomString": func() (string, error) { return utilsx.RandomString(16) },
"RandomHex": func() (string, error) { return utilsx.RandomHex(16) },
"RandomBase64": func() (string, error) { return utilsx.RandomBase64(16) },
"RandomToken": func() (string, error) { return utilsx.RandomToken(24) },
"APIKey": func() (string, error) { return utilsx.APIKey("sk.", 24) },
"UUID": utilsx.UUID,
}
for name, gen := range generators {
first, err := gen()
if err != nil {
t.Fatalf("%s 返回错误: %v", name, err)
}
allSame := true
for i := 0; i < 32; i++ {
v, err := gen()
if err != nil {
t.Fatalf("%s 返回错误: %v", name, err)
}
if v != first {
allSame = false
break
}
}
if allSame {
t.Errorf("%s 32 次调用返回了完全相同的结果,随机源可疑", name)
}
2025-11-03 23:40:41 +08:00
}
}
2026-09-30 15:49:12 +08:00
// TestRandomBytesUsesCryptoRand 交叉验证:用 crypto/rand 自身生成的字节
// 与 RandomBytes 的输出空间应当一致(都覆盖全字节值,排除 math/rand 之外的弱源)。
func TestRandomBytesStatisticalSmoke(t *testing.T) {
buf, err := utilsx.RandomBytes(4096)
if err != nil {
t.Fatalf("RandomBytes 返回错误: %v", err)
}
var counts [256]int
for _, b := range buf {
counts[b]++
}
distinct := 0
for _, c := range counts {
if c > 0 {
distinct++
}
}
// 4096 字节取样下几乎必然覆盖 256 个值中的绝大多数;此处用宽松阈值避免偶发失败。
if distinct < 200 {
t.Errorf("4096 字节中只出现 %d 种字节值,随机性可疑", distinct)
}
if bytes.Equal(buf, make([]byte, len(buf))) {
t.Error("RandomBytes 返回了全零内容")
}
// 与 crypto/rand 直接读取的结果长度一致(仅作 API 对照,不比较内容)。
ref := make([]byte, 8)
if _, err := rand.Read(ref); err != nil {
t.Fatalf("crypto/rand 读取失败: %v", err)
}
if len(ref) != 8 {
t.Error("crypto/rand 对照读取长度异常")
}
}
// assertUnique 校验 gen 连续调用 count 次都不产生重复值。
func assertUnique(t *testing.T, count int, gen func() string) {
t.Helper()
seen := make(map[string]struct{}, count)
for i := 0; i < count; i++ {
v := gen()
if _, ok := seen[v]; ok {
t.Fatalf("第 %d 次调用产生了重复值(长度 %d)", i, len(v))
}
seen[v] = struct{}{}
}
}
// assertOnlyCharset 校验字符串只包含给定字符集中的字符。
func assertOnlyCharset(t *testing.T, s, charset string) {
t.Helper()
for i := 0; i < len(s); i++ {
if strings.IndexByte(charset, s[i]) < 0 {
t.Fatalf("字符串含字符集外的字符 %q", s[i])
}
2025-11-03 23:40:41 +08:00
}
}