462 lines
12 KiB
Go
462 lines
12 KiB
Go
package validation_test
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/validation"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
// ===== Password Tests =====
|
||
|
||
func TestValidatePassword(t *testing.T) {
|
||
tests := []struct {
|
||
password string
|
||
valid bool
|
||
msg string
|
||
}{
|
||
{"Abc12345", true, ""}, // 有效
|
||
{"abc12345", false, "密码必须包含大写字母"}, // 缺大写
|
||
{"ABC12345", false, "密码必须包含小写字母"}, // 缺小写
|
||
{"Abcdefgh", false, "密码必须包含数字"}, // 缺数字
|
||
{"Abc123", false, "密码长度不能少于8位"}, // 太短
|
||
{"", false, "密码长度不能少于8位"}, // 空
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
valid, msg := validation.ValidatePassword(tt.password)
|
||
if valid != tt.valid {
|
||
t.Errorf("ValidatePassword(%s) valid = %v, want %v", tt.password, valid, tt.valid)
|
||
}
|
||
if !valid && msg != tt.msg {
|
||
t.Errorf("ValidatePassword(%s) msg = %s, want %s", tt.password, msg, tt.msg)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestValidatePasswordWithConfig(t *testing.T) {
|
||
// 自定义配置:不要求特殊字符
|
||
config := validation.PasswordConfig{
|
||
MinLength: 6,
|
||
MaxLength: 20,
|
||
RequireUpper: false,
|
||
RequireLower: true,
|
||
RequireDigit: true,
|
||
RequireSpecial: false,
|
||
}
|
||
|
||
valid, msg := validation.ValidatePasswordWithConfig("abc123", config)
|
||
if !valid {
|
||
t.Errorf("ValidatePasswordWithConfig should be valid: %s", msg)
|
||
}
|
||
|
||
// 要求特殊字符
|
||
config2 := validation.PasswordConfig{
|
||
MinLength: 8,
|
||
MaxLength: 20,
|
||
RequireUpper: true,
|
||
RequireLower: true,
|
||
RequireDigit: true,
|
||
RequireSpecial: true,
|
||
}
|
||
|
||
valid2, msg2 := validation.ValidatePasswordWithConfig("Abc12345", config2)
|
||
if valid2 {
|
||
t.Error("Should require special character")
|
||
}
|
||
if msg2 != "密码必须包含特殊字符" {
|
||
t.Errorf("msg = %s, want '密码必须包含特殊字符'", msg2)
|
||
}
|
||
|
||
// 包含特殊字符
|
||
valid3, msg3 := validation.ValidatePasswordWithConfig("Abc123!@#", config2)
|
||
if !valid3 {
|
||
t.Errorf("Should be valid: %s", msg3)
|
||
}
|
||
}
|
||
|
||
func TestDefaultPasswordConfig(t *testing.T) {
|
||
cfg := validation.DefaultPasswordConfig
|
||
if cfg.MinLength != 8 {
|
||
t.Errorf("MinLength = %d, want 8", cfg.MinLength)
|
||
}
|
||
if cfg.MaxLength != 72 {
|
||
t.Errorf("MaxLength = %d, want 72 (P1 #17: bcrypt 72-byte truncation boundary)", cfg.MaxLength)
|
||
}
|
||
if !cfg.RequireUpper || !cfg.RequireLower || !cfg.RequireDigit {
|
||
t.Error("Default config should require upper, lower, digit")
|
||
}
|
||
}
|
||
|
||
// ===== Hash Password Tests =====
|
||
|
||
func TestHashPassword(t *testing.T) {
|
||
password := "testPassword123"
|
||
|
||
hash, err := validation.HashPassword(password)
|
||
if err != nil {
|
||
t.Fatalf("HashPassword error: %v", err)
|
||
}
|
||
|
||
// 验证 hash 不等于原密码
|
||
if hash == password {
|
||
t.Error("Hash should not equal original password")
|
||
}
|
||
|
||
// 验证 hash 非空
|
||
if hash == "" {
|
||
t.Error("Hash should not be empty")
|
||
}
|
||
|
||
// 验证可以匹配
|
||
if !validation.CheckPassword(hash, password) {
|
||
t.Error("CheckPassword should return true for correct password")
|
||
}
|
||
|
||
// 验证错误密码不匹配
|
||
if validation.CheckPassword(hash, "wrongPassword") {
|
||
t.Error("CheckPassword should return false for wrong password")
|
||
}
|
||
}
|
||
|
||
func TestHashPasswordWithCost(t *testing.T) {
|
||
password := "testPassword123"
|
||
|
||
// 测试不同 cost
|
||
hash, err := validation.HashPasswordWithCost(password, 10)
|
||
if err != nil {
|
||
t.Fatalf("HashPasswordWithCost error: %v", err)
|
||
}
|
||
|
||
cost, err := validation.GetPasswordCost(hash)
|
||
if err != nil {
|
||
t.Fatalf("GetPasswordCost error: %v", err)
|
||
}
|
||
if cost != 10 {
|
||
t.Errorf("Cost = %d, want 10", cost)
|
||
}
|
||
|
||
// 测试 cost 边界
|
||
hash2, err := validation.HashPasswordWithCost(password, 3) // 低于 MinCost
|
||
if err != nil {
|
||
t.Fatalf("HashPasswordWithCost with low cost error: %v", err)
|
||
}
|
||
cost2, _ := validation.GetPasswordCost(hash2)
|
||
if cost2 < bcrypt.MinCost {
|
||
t.Errorf("Cost should be at least MinCost")
|
||
}
|
||
}
|
||
|
||
func TestCheckPasswordAndUpgrade(t *testing.T) {
|
||
password := "testPassword123"
|
||
|
||
// 使用低 cost 创建 hash
|
||
hash, _ := validation.HashPasswordWithCost(password, 4)
|
||
|
||
// 检查并尝试升级
|
||
match, needUpgrade, newHash, err := validation.CheckPasswordAndUpgrade(hash, password, 12)
|
||
if err != nil {
|
||
t.Fatalf("CheckPasswordAndUpgrade error: %v", err)
|
||
}
|
||
|
||
if !match {
|
||
t.Error("Password should match")
|
||
}
|
||
|
||
if !needUpgrade {
|
||
t.Error("Should need upgrade (cost 4 -> 12)")
|
||
}
|
||
|
||
if newHash == "" {
|
||
t.Error("New hash should be provided")
|
||
}
|
||
|
||
// 验证新 hash 可用
|
||
if !validation.CheckPassword(newHash, password) {
|
||
t.Error("New hash should work")
|
||
}
|
||
|
||
// 使用高 cost,不需要升级
|
||
hash2, _ := validation.HashPasswordWithCost(password, 12)
|
||
match2, needUpgrade2, _, _ := validation.CheckPasswordAndUpgrade(hash2, password, 12)
|
||
if !match2 {
|
||
t.Error("Password should match")
|
||
}
|
||
if needUpgrade2 {
|
||
t.Error("Should not need upgrade")
|
||
}
|
||
|
||
// 错误密码
|
||
match3, _, _, _ := validation.CheckPasswordAndUpgrade(hash, "wrongPassword", 12)
|
||
if match3 {
|
||
t.Error("Wrong password should not match")
|
||
}
|
||
}
|
||
|
||
func TestCheckPasswordAndUpgradeNormalizesInvalidTargetCost(t *testing.T) {
|
||
password := "testPassword123"
|
||
hash, err := validation.HashPasswordWithCost(password, 12)
|
||
if err != nil {
|
||
t.Fatalf("HashPasswordWithCost: %v", err)
|
||
}
|
||
|
||
match, needUpgrade, newHash, err := validation.CheckPasswordAndUpgrade(hash, password, bcrypt.MaxCost+1)
|
||
if err != nil {
|
||
t.Fatalf("CheckPasswordAndUpgrade with high target cost: %v", err)
|
||
}
|
||
if !match {
|
||
t.Fatal("Password should match")
|
||
}
|
||
if needUpgrade || newHash != "" {
|
||
t.Fatalf("invalid high target cost should normalize to default and avoid upgrade, needUpgrade=%v newHash=%q", needUpgrade, newHash)
|
||
}
|
||
}
|
||
|
||
func TestGetPasswordCost(t *testing.T) {
|
||
password := "testPassword123"
|
||
hash, _ := validation.HashPasswordWithCost(password, 12)
|
||
|
||
cost, err := validation.GetPasswordCost(hash)
|
||
if err != nil {
|
||
t.Fatalf("GetPasswordCost error: %v", err)
|
||
}
|
||
if cost != 12 {
|
||
t.Errorf("Cost = %d, want 12", cost)
|
||
}
|
||
|
||
// 无效 hash
|
||
_, err = validation.GetPasswordCost("invalidHash")
|
||
if err == nil {
|
||
t.Error("GetPasswordCost should fail with invalid hash")
|
||
}
|
||
}
|
||
|
||
// ===== Validation Errors Tests =====
|
||
|
||
func TestValidationErrors(t *testing.T) {
|
||
errors := validation.ValidationErrors{
|
||
{Field: "name", Label: "姓名", Message: "必填"},
|
||
{Field: "email", Label: "邮箱", Message: "格式错误"},
|
||
}
|
||
|
||
// Error 方法
|
||
errStr := errors.Error()
|
||
if errStr != "姓名: 必填; 邮箱: 格式错误" {
|
||
t.Errorf("Error() = %s", errStr)
|
||
}
|
||
|
||
// ToMap 方法
|
||
m := errors.ToMap()
|
||
if m["name"] != "必填" {
|
||
t.Error("ToMap failed")
|
||
}
|
||
|
||
// ToLabelMap 方法
|
||
lm := errors.ToLabelMap()
|
||
if lm["姓名"] != "必填" {
|
||
t.Error("ToLabelMap failed")
|
||
}
|
||
|
||
// First 方法
|
||
first := errors.First()
|
||
if first.Field != "name" {
|
||
t.Error("First failed")
|
||
}
|
||
|
||
// FirstMessage 方法
|
||
msg := errors.FirstMessage()
|
||
if msg != "必填" {
|
||
t.Errorf("FirstMessage = %s", msg)
|
||
}
|
||
|
||
// 空 errors
|
||
emptyErrors := validation.ValidationErrors{}
|
||
if emptyErrors.First() != nil {
|
||
t.Error("Empty First should return nil")
|
||
}
|
||
if emptyErrors.FirstMessage() != "" {
|
||
t.Error("Empty FirstMessage should return empty")
|
||
}
|
||
}
|
||
|
||
// ===== Struct Validation Tests =====
|
||
|
||
type TestUser struct {
|
||
Name string `json:"name" label:"姓名" binding:"required" msg_required:"姓名不能为空"`
|
||
Email string `json:"email" label:"邮箱" binding:"required,email" msg_required:"邮箱不能为空" msg_email:"邮箱格式不正确"`
|
||
Age int `json:"age" label:"年龄" binding:"gte=0,lte=150"`
|
||
Password string `json:"password" binding:"min=8" msg_min:"密码至少8位"`
|
||
}
|
||
|
||
func TestValidateStruct(t *testing.T) {
|
||
validation.InitValidator()
|
||
|
||
// 有效数据
|
||
validUser := TestUser{
|
||
Name: "张三",
|
||
Email: "test@example.com",
|
||
Age: 25,
|
||
Password: "password123",
|
||
}
|
||
|
||
errors := validation.ValidateStruct(validUser)
|
||
if errors != nil {
|
||
t.Errorf("Valid struct should have no errors: %v", errors)
|
||
}
|
||
|
||
// 无效数据
|
||
invalidUser := TestUser{
|
||
Name: "",
|
||
Email: "invalid-email",
|
||
Age: 200,
|
||
Password: "short",
|
||
}
|
||
|
||
errors2 := validation.ValidateStruct(invalidUser)
|
||
if errors2 == nil {
|
||
t.Error("Invalid struct should have errors")
|
||
}
|
||
|
||
// 检查错误数量
|
||
if len(errors2) < 3 {
|
||
t.Errorf("Should have at least 3 errors, got %d", len(errors2))
|
||
}
|
||
|
||
// 检查自定义消息
|
||
firstMsg := errors2.FirstMessage()
|
||
if firstMsg != "姓名不能为空" {
|
||
t.Errorf("FirstMessage = %s, want '姓名不能为空'", firstMsg)
|
||
}
|
||
}
|
||
|
||
func TestValidateStructNil(t *testing.T) {
|
||
if errors := validation.ValidateStruct(nil); errors != nil {
|
||
t.Fatalf("ValidateStruct(nil) = %v, want nil", errors)
|
||
}
|
||
|
||
// 空结构体
|
||
emptyUser := TestUser{}
|
||
errors := validation.ValidateStruct(emptyUser)
|
||
if errors == nil {
|
||
t.Error("Empty struct should have errors")
|
||
}
|
||
}
|
||
|
||
type TestPhone struct {
|
||
Phone string `json:"phone" binding:"phone" msg_phone:"手机号格式不正确"`
|
||
}
|
||
|
||
func TestValidatePhone(t *testing.T) {
|
||
validation.InitValidator()
|
||
|
||
// 有效手机号
|
||
valid := TestPhone{Phone: "13812345678"}
|
||
errors := validation.ValidateStruct(valid)
|
||
if errors != nil {
|
||
t.Errorf("Valid phone should pass: %v", errors)
|
||
}
|
||
|
||
// 无效手机号 - 长度错误
|
||
invalidLen := TestPhone{Phone: "1234567"}
|
||
errors2 := validation.ValidateStruct(invalidLen)
|
||
if errors2 == nil {
|
||
t.Error("Invalid phone length should fail")
|
||
}
|
||
|
||
// 无效手机号 - 不以1开头
|
||
invalidPrefix := TestPhone{Phone: "23812345678"}
|
||
errors3 := validation.ValidateStruct(invalidPrefix)
|
||
if errors3 == nil {
|
||
t.Error("Phone not starting with 1 should fail")
|
||
}
|
||
|
||
// P1 #17:长度合规、以 1 开头但含非数字字符必须拒绝(原实现会误通过)。
|
||
invalidNonDigit := TestPhone{Phone: "1abcdefghij"}
|
||
if errs := validation.ValidateStruct(invalidNonDigit); errs == nil {
|
||
t.Error("Phone with non-digit chars should fail (P1 #17)")
|
||
}
|
||
}
|
||
|
||
type TestUsername struct {
|
||
Username string `json:"username" binding:"username" msg_username:"用户名格式不正确"`
|
||
}
|
||
|
||
func TestValidateUsername(t *testing.T) {
|
||
validation.InitValidator()
|
||
|
||
tests := []struct {
|
||
username string
|
||
valid bool
|
||
}{
|
||
{"abc123", true}, // 有效
|
||
{"Abc123", true}, // 有效(大写开头)
|
||
{"user_name", true}, // 有效(包含下划线)
|
||
{"123abc", false}, // 无效(数字开头)
|
||
{"ab", false}, // 无效(太短)
|
||
{"a!bc", false}, // 无效(特殊字符)
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
u := TestUsername{Username: tt.username}
|
||
errors := validation.ValidateStruct(u)
|
||
valid := errors == nil
|
||
if valid != tt.valid {
|
||
t.Errorf("Username %s: valid=%v, want %v", tt.username, valid, tt.valid)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestIDCardChecksum_M5:18 位身份证须校验校验位(修复前仅查长度+格式)。
|
||
func TestIDCardChecksum_M5(t *testing.T) {
|
||
validation.InitValidator()
|
||
|
||
type TestIDCard struct {
|
||
ID string `json:"id" binding:"idcard"`
|
||
}
|
||
tests := []struct {
|
||
id string
|
||
valid bool
|
||
}{
|
||
{"11010519491231002X", true}, // 合法(校验位正确)
|
||
{"110101199003078478", true}, // 合法(校验位正确)
|
||
{"110101199003078475", false}, // 校验位错误(末位改坏)
|
||
{"11010119900307847X", false}, // 18 位但校验位错
|
||
{"123456789012345", true}, // 15 位旧号段仅查格式
|
||
{"123456789012345678", false}, // 18 位但校验位错
|
||
{"123", false}, // 长度不符
|
||
}
|
||
for _, tt := range tests {
|
||
u := TestIDCard{ID: tt.id}
|
||
errs := validation.ValidateStruct(u)
|
||
valid := errs == nil
|
||
if valid != tt.valid {
|
||
t.Errorf("IDCard %s: valid=%v, want %v", tt.id, valid, tt.valid)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== Benchmarks =====
|
||
|
||
func BenchmarkHashPassword(b *testing.B) {
|
||
password := "testPassword123"
|
||
for i := 0; i < b.N; i++ {
|
||
validation.HashPassword(password)
|
||
}
|
||
}
|
||
|
||
func BenchmarkCheckPassword(b *testing.B) {
|
||
password := "testPassword123"
|
||
hash, _ := validation.HashPassword(password)
|
||
b.ResetTimer()
|
||
for i := 0; i < b.N; i++ {
|
||
validation.CheckPassword(hash, password)
|
||
}
|
||
}
|
||
|
||
func BenchmarkValidatePassword(b *testing.B) {
|
||
password := "TestPassword123"
|
||
for i := 0; i < b.N; i++ {
|
||
validation.ValidatePassword(password)
|
||
}
|
||
}
|