Files
encryptx/utilsx/utilsx_test.go
T
2026-09-30 15:49:12 +08:00

728 lines
22 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 utilsx_test
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"strings"
"testing"
"code.yun.ink/pkg/encryptx/utilsx"
)
// urlSafeAlphabet 是 URL 安全 Base64(无填充)允许出现的全部字符。
const urlSafeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
// 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))
}
}
// 错误路径: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))
}
}
// 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)
})
}
// TestRandomString 校验字符集、长度、随机性与错误路径。
func TestRandomString(t *testing.T) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
s, err := utilsx.RandomString(32)
if err != nil {
t.Fatalf("RandomString 返回错误: %v", err)
}
if len(s) != 32 {
t.Errorf("RandomString(32) 长度 = %d", len(s))
}
for i := 0; i < len(s); i++ {
if strings.IndexByte(charset, s[i]) < 0 {
t.Errorf("RandomString 含非法字符 %q", s[i])
}
}
assertUnique(t, 64, func() string {
v, err := utilsx.RandomString(24)
if err != nil {
t.Fatalf("RandomString 返回错误: %v", err)
}
return v
})
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])
}
}
// 单字符字符集应始终返回同内容但长度正确的串。
single, err := utilsx.RandomStringWithCharset(8, "x")
if err != nil {
t.Fatalf("单字符字符集出错: %v", err)
}
if single != "xxxxxxxx" {
t.Errorf("单字符字符集结果 = %q, 期望 xxxxxxxx", single)
}
// 空字符集必须报错。
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)
}
}
assertUnique(t, 64, func() string {
v, err := utilsx.RandomBase64(24)
if err != nil {
t.Fatalf("RandomBase64 返回错误: %v", err)
}
return v
})
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)
}
}
}
// 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)
// 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)
}
}
// 低于最小长度必须报错(且返回空串、不 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)
}
}()
}
// 超过上限必须报错。
if _, err := utilsx.RandomToken((1 << 20) + 1); err == nil {
t.Error("RandomToken 超上限应返回错误")
}
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)
}
}
}
}
// 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++
}
}
// 若未洗牌则 firstIsUpper == iterations;4 类均匀时期望约 1/4。
if firstIsUpper == iterations {
t.Error("首位始终是大写字母,说明 Fisher-Yates 洗牌未生效")
}
}
// 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)
}
}
}
// 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)
}
}
}
// 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))
}
}
// 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
}
}
// 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")
}
}
// 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")
}
}
// 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)
}
}
}
// 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])
}
}
}