728 lines
22 KiB
Go
728 lines
22 KiB
Go
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])
|
||
}
|
||
}
|
||
}
|