357 lines
9.9 KiB
Go
357 lines
9.9 KiB
Go
|
|
package service_test
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
"github.com/user-management-system/internal/domain"
|
|||
|
|
"github.com/user-management-system/internal/service"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// =============================================================================
|
|||
|
|
// 边界值测试 - 使用TDD方法确保健壮性
|
|||
|
|
// =============================================================================
|
|||
|
|
|
|||
|
|
// TestBoundary_UsernameLength 用户名长度边界测试
|
|||
|
|
func TestBoundary_UsernameLength(t *testing.T) {
|
|||
|
|
env := setupTestEnv(t)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
username string
|
|||
|
|
wantErr bool
|
|||
|
|
errMsg string
|
|||
|
|
}{
|
|||
|
|
{"空用户名", "", true, "用户名不能为空"},
|
|||
|
|
{"单字符", "a", false, ""},
|
|||
|
|
{"最小有效长度", "ab", false, ""},
|
|||
|
|
{"正常长度", "normaluser", false, ""},
|
|||
|
|
{"最大有效长度-50", strings.Repeat("a", 50), false, ""},
|
|||
|
|
{"超过最大长度-51", strings.Repeat("a", 51), true, "用户名长度超过限制"},
|
|||
|
|
{"超长字符串-1000", strings.Repeat("a", 1000), true, "用户名长度超过限制"},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: tt.username,
|
|||
|
|
Password: "$2a$10$dummy",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
err := env.userSvc.Create(ctx, user)
|
|||
|
|
|
|||
|
|
if tt.wantErr {
|
|||
|
|
if err == nil {
|
|||
|
|
t.Errorf("期望错误但没有返回: %s", tt.errMsg)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if err != nil {
|
|||
|
|
t.Errorf("不期望错误但返回: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBoundary_EmailFormat 邮箱格式边界测试
|
|||
|
|
func TestBoundary_EmailFormat(t *testing.T) {
|
|||
|
|
env := setupTestEnv(t)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
email string
|
|||
|
|
wantOK bool
|
|||
|
|
comment string
|
|||
|
|
}{
|
|||
|
|
{"空邮箱", "", true, "邮箱为可选字段"},
|
|||
|
|
{"正常邮箱", "user@example.com", true, "标准格式"},
|
|||
|
|
{"带子域名", "user@mail.example.com", true, "多级域名"},
|
|||
|
|
{"带加号", "user+tag@example.com", true, "Gmail风格"},
|
|||
|
|
{"无@符号", "userexample.com", false, "缺少@"},
|
|||
|
|
{"无域名", "user@", false, "缺少域名"},
|
|||
|
|
{"无用户名", "@example.com", false, "缺少用户名"},
|
|||
|
|
{"多个@", "user@@example.com", false, "多个@符号"},
|
|||
|
|
{"空格", "user @example.com", false, "包含空格"},
|
|||
|
|
{"超长邮箱", strings.Repeat("a", 100) + "@example.com", false, "超过长度限制"},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: "test_" + strings.ReplaceAll(tt.name, " ", "_"),
|
|||
|
|
Email: strPtr(tt.email),
|
|||
|
|
Password: "$2a$10$dummy",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
err := env.userSvc.Create(ctx, user)
|
|||
|
|
|
|||
|
|
if tt.wantOK {
|
|||
|
|
if err != nil {
|
|||
|
|
t.Errorf("邮箱 '%s' 应该被接受但返回错误: %v (%s)", tt.email, err, tt.comment)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if err == nil {
|
|||
|
|
t.Errorf("邮箱 '%s' 应该被拒绝但接受了 (%s)", tt.email, tt.comment)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBoundary_PasswordStrength 密码强度边界测试
|
|||
|
|
func TestBoundary_PasswordStrength(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
password string
|
|||
|
|
wantOK bool
|
|||
|
|
comment string
|
|||
|
|
}{
|
|||
|
|
{"空密码", "", false, "必须设置密码"},
|
|||
|
|
{"仅数字", "12345678", false, "需要复杂度"},
|
|||
|
|
{"仅小写", "abcdefgh", false, "需要大写"},
|
|||
|
|
{"仅大写", "ABCDEFGH", false, "需要小写"},
|
|||
|
|
{"字母数字", "Password12", false, "需要特殊字符"},
|
|||
|
|
{"最小有效密码", "Pass123!", true, "8位,包含大小写数字特殊字符"},
|
|||
|
|
{"强密码", "Str0ng@Pass!", true, "12位,高复杂度"},
|
|||
|
|
{"超长密码", strings.Repeat("Aa1!", 50), true, "200字符"},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
// 密码验证通常在handler层,这里验证服务层行为
|
|||
|
|
if tt.wantOK {
|
|||
|
|
t.Logf("✓ 密码 '%s' 符合强度要求 (%s)", tt.password[:min(10, len(tt.password))], tt.comment)
|
|||
|
|
} else {
|
|||
|
|
t.Logf("✗ 密码 '%s' 不符合强度要求 (%s)", tt.password, tt.comment)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBoundary_PaginationParams 分页参数边界测试
|
|||
|
|
func TestBoundary_PaginationParams(t *testing.T) {
|
|||
|
|
env := setupTestEnv(t)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
// 先创建一些测试数据
|
|||
|
|
for i := 0; i < 15; i++ {
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: "pageuser_" + strings.Repeat("0", 2-len(string(rune('0'+i)))) + string(rune('0'+i)),
|
|||
|
|
Password: "$2a$10$dummy",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
env.userSvc.Create(ctx, user)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
page int
|
|||
|
|
pageSize int
|
|||
|
|
wantCount int
|
|||
|
|
wantTotal int64
|
|||
|
|
}{
|
|||
|
|
{"第一页", 1, 10, 10, 15},
|
|||
|
|
{"第二页", 2, 10, 5, 15},
|
|||
|
|
{"空页", 3, 10, 0, 15},
|
|||
|
|
{"页面大小1", 1, 1, 1, 15},
|
|||
|
|
{"大页面", 1, 100, 15, 15},
|
|||
|
|
{"零页-应默认为1", 0, 10, 10, 15},
|
|||
|
|
{"负页-应默认为1", -1, 10, 10, 15},
|
|||
|
|
{"零页面大小-应默认", 1, 0, 10, 15},
|
|||
|
|
{"负页面大小-应默认", 1, -10, 10, 15},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
users, total, err := env.userSvc.List(ctx, (tt.page-1)*tt.pageSize, tt.pageSize)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("List失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(users) != tt.wantCount {
|
|||
|
|
t.Errorf("期望 %d 条记录,得到 %d", tt.wantCount, len(users))
|
|||
|
|
}
|
|||
|
|
if total < tt.wantTotal {
|
|||
|
|
t.Errorf("总数至少应为 %d,得到 %d", tt.wantTotal, total)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBoundary_StatusTransition 状态转换边界测试
|
|||
|
|
func TestBoundary_StatusTransition(t *testing.T) {
|
|||
|
|
env := setupTestEnv(t)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
fromStatus domain.UserStatus
|
|||
|
|
toStatus domain.UserStatus
|
|||
|
|
wantOK bool
|
|||
|
|
}{
|
|||
|
|
{"激活->禁用", domain.UserStatusActive, domain.UserStatusDisabled, true},
|
|||
|
|
{"激活->锁定", domain.UserStatusActive, domain.UserStatusLocked, true},
|
|||
|
|
{"激活->未激活", domain.UserStatusActive, domain.UserStatusInactive, true},
|
|||
|
|
{"禁用->激活", domain.UserStatusDisabled, domain.UserStatusActive, true},
|
|||
|
|
{"锁定->激活", domain.UserStatusLocked, domain.UserStatusActive, true},
|
|||
|
|
{"未激活->激活", domain.UserStatusInactive, domain.UserStatusActive, true},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: "status_" + strings.ReplaceAll(tt.name, "->", "_"),
|
|||
|
|
Password: "$2a$10$dummy",
|
|||
|
|
Status: tt.fromStatus,
|
|||
|
|
}
|
|||
|
|
if err := env.userSvc.Create(ctx, user); err != nil {
|
|||
|
|
t.Fatalf("创建用户失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err := env.userSvc.UpdateStatus(ctx, user.ID, tt.toStatus)
|
|||
|
|
if tt.wantOK && err != nil {
|
|||
|
|
t.Errorf("状态转换 %v->%v 应该成功但失败: %v", tt.fromStatus, tt.toStatus, err)
|
|||
|
|
}
|
|||
|
|
if !tt.wantOK && err == nil {
|
|||
|
|
t.Errorf("状态转换 %v->%v 应该失败但成功", tt.fromStatus, tt.toStatus)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBoundary_UserID 用户ID边界测试
|
|||
|
|
func TestBoundary_UserID(t *testing.T) {
|
|||
|
|
env := setupTestEnv(t)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
// 先创建一个有效用户
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: "valid_user_for_id_test",
|
|||
|
|
Password: "$2a$10$dummy",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
env.userSvc.Create(ctx, user)
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
userID int64
|
|||
|
|
wantErr bool
|
|||
|
|
}{
|
|||
|
|
{"零ID", 0, true},
|
|||
|
|
{"负ID", -1, true},
|
|||
|
|
{"有效ID", user.ID, false},
|
|||
|
|
{"超大ID", 9223372036854775807, true}, // int64 max
|
|||
|
|
{"不存在的ID", 999999999, true},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
_, err := env.userSvc.GetByID(ctx, tt.userID)
|
|||
|
|
if tt.wantErr && err == nil {
|
|||
|
|
t.Error("期望错误但没有返回")
|
|||
|
|
}
|
|||
|
|
if !tt.wantErr && err != nil {
|
|||
|
|
t.Errorf("不期望错误但返回: %v", err)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBoundary_BatchOperations 批量操作边界测试
|
|||
|
|
func TestBoundary_BatchOperations(t *testing.T) {
|
|||
|
|
env := setupTestEnv(t)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
// 创建测试用户
|
|||
|
|
var userIDs []int64
|
|||
|
|
for i := 0; i < 5; i++ {
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: "batch_user_" + string(rune('0'+i)),
|
|||
|
|
Password: "$2a$10$dummy",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
env.userSvc.Create(ctx, user)
|
|||
|
|
userIDs = append(userIDs, user.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
ids []int64
|
|||
|
|
wantErr bool
|
|||
|
|
}{
|
|||
|
|
{"空ID列表", []int64{}, false},
|
|||
|
|
{"单个ID", []int64{userIDs[0]}, false},
|
|||
|
|
{"多个ID", userIDs[:3], false},
|
|||
|
|
{"重复ID", []int64{userIDs[0], userIDs[0], userIDs[1], userIDs[1]}, false}, // 应该去重
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
// 批量状态更新
|
|||
|
|
_, err := env.userSvc.BatchUpdateStatus(ctx, &service.BatchUpdateStatusRequest{
|
|||
|
|
IDs: tt.ids,
|
|||
|
|
Status: domain.UserStatusInactive,
|
|||
|
|
})
|
|||
|
|
if tt.wantErr && err == nil {
|
|||
|
|
t.Error("期望错误但没有返回")
|
|||
|
|
}
|
|||
|
|
if !tt.wantErr && err != nil {
|
|||
|
|
t.Errorf("不期望错误但返回: %v", err)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBoundary_StringLength 字符串长度边界测试
|
|||
|
|
func TestBoundary_StringLength(t *testing.T) {
|
|||
|
|
env := setupTestEnv(t)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
nickname string
|
|||
|
|
region string
|
|||
|
|
bio string
|
|||
|
|
wantError bool
|
|||
|
|
}{
|
|||
|
|
{"正常长度", "正常昵称", "北京", "这是个人简介", false},
|
|||
|
|
{"空字符串", "", "", "", false},
|
|||
|
|
{"最大昵称长度50", strings.Repeat("测", 50), "", "", false},
|
|||
|
|
{"超过昵称长度", strings.Repeat("测", 51), "", "", true},
|
|||
|
|
{"最大简介长度500", "", "", strings.Repeat("测", 500), false},
|
|||
|
|
{"超过简介长度", "", "", strings.Repeat("测", 501), true},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: "str_test_" + strings.ReplaceAll(tt.name, " ", "_"),
|
|||
|
|
Password: "$2a$10$dummy",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
Nickname: tt.nickname,
|
|||
|
|
Region: tt.region,
|
|||
|
|
Bio: tt.bio,
|
|||
|
|
}
|
|||
|
|
err := env.userSvc.Create(ctx, user)
|
|||
|
|
|
|||
|
|
if tt.wantError && err == nil {
|
|||
|
|
t.Error("期望错误但没有返回")
|
|||
|
|
}
|
|||
|
|
if !tt.wantError && err != nil {
|
|||
|
|
t.Errorf("不期望错误但返回: %v", err)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 辅助函数
|
|||
|
|
func min(a, b int) int {
|
|||
|
|
if a < b {
|
|||
|
|
return a
|
|||
|
|
}
|
|||
|
|
return b
|
|||
|
|
}
|