refactor: Implement dependency injection for handlers and services
- Refactored AuthHandler, UserHandler, TextureHandler, ProfileHandler, CaptchaHandler, and YggdrasilHandler to use dependency injection. - Removed direct instantiation of services and repositories within handlers, replacing them with constructor injection. - Updated the container to initialize service instances and provide them to handlers. - Enhanced code structure for better testability and adherence to Go best practices.
This commit is contained in:
@@ -1,199 +1,378 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/auth"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestGetDefaultAvatar 测试获取默认头像的逻辑
|
||||
// 注意:这个测试需要mock repository,但由于repository是函数式的,
|
||||
// 我们只测试逻辑部分
|
||||
func TestGetDefaultAvatar_Logic(t *testing.T) {
|
||||
func TestUserServiceImpl_Register(t *testing.T) {
|
||||
// 准备依赖
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
// 初始化Service
|
||||
// 注意:redisClient 传入 nil,因为 Register 方法中没有使用 redis
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
|
||||
// 测试用例
|
||||
tests := []struct {
|
||||
name string
|
||||
configExists bool
|
||||
configValue string
|
||||
expectedResult string
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
email string
|
||||
avatar string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
setupMocks func()
|
||||
}{
|
||||
{
|
||||
name: "配置存在时返回配置值",
|
||||
configExists: true,
|
||||
configValue: "https://example.com/avatar.png",
|
||||
expectedResult: "https://example.com/avatar.png",
|
||||
name: "正常注册",
|
||||
username: "testuser",
|
||||
password: "password123",
|
||||
email: "test@example.com",
|
||||
avatar: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "配置不存在时返回错误信息",
|
||||
configExists: false,
|
||||
configValue: "",
|
||||
expectedResult: "数据库中不存在默认头像配置",
|
||||
name: "用户名已存在",
|
||||
username: "existinguser",
|
||||
password: "password123",
|
||||
email: "new@example.com",
|
||||
avatar: "",
|
||||
wantErr: true,
|
||||
errMsg: "用户名已存在",
|
||||
setupMocks: func() {
|
||||
userRepo.Create(&model.User{
|
||||
Username: "existinguser",
|
||||
Email: "old@example.com",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "邮箱已存在",
|
||||
username: "newuser",
|
||||
password: "password123",
|
||||
email: "existing@example.com",
|
||||
avatar: "",
|
||||
wantErr: true,
|
||||
errMsg: "邮箱已被注册",
|
||||
setupMocks: func() {
|
||||
userRepo.Create(&model.User{
|
||||
Username: "otheruser",
|
||||
Email: "existing@example.com",
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 这个测试只验证逻辑,不实际调用repository
|
||||
// 实际的repository调用测试需要集成测试或mock
|
||||
if tt.configExists {
|
||||
if tt.expectedResult != tt.configValue {
|
||||
t.Errorf("当配置存在时,应该返回配置值")
|
||||
// 重置mock状态
|
||||
if tt.setupMocks != nil {
|
||||
tt.setupMocks()
|
||||
}
|
||||
|
||||
user, token, err := userService.Register(tt.username, tt.password, tt.email, tt.avatar)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("期望返回错误,但实际没有错误")
|
||||
return
|
||||
}
|
||||
if tt.errMsg != "" && err.Error() != tt.errMsg {
|
||||
t.Errorf("错误信息不匹配: got %v, want %v", err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
if !strings.Contains(tt.expectedResult, "数据库中不存在默认头像配置") {
|
||||
t.Errorf("当配置不存在时,应该返回错误信息")
|
||||
if err != nil {
|
||||
t.Errorf("不期望返回错误: %v", err)
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
t.Error("返回的用户不应为nil")
|
||||
}
|
||||
if token == "" {
|
||||
t.Error("返回的Token不应为空")
|
||||
}
|
||||
if user.Username != tt.username {
|
||||
t.Errorf("用户名不匹配: got %v, want %v", user.Username, tt.username)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoginUser_EmailDetection 测试登录时邮箱检测逻辑
|
||||
func TestLoginUser_EmailDetection(t *testing.T) {
|
||||
func TestUserServiceImpl_Login(t *testing.T) {
|
||||
// 准备依赖
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
// 预置用户
|
||||
password := "password123"
|
||||
hashedPassword, _ := auth.HashPassword(password)
|
||||
testUser := &model.User{
|
||||
Username: "testlogin",
|
||||
Email: "login@example.com",
|
||||
Password: hashedPassword,
|
||||
Status: 1,
|
||||
}
|
||||
userRepo.Create(testUser)
|
||||
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
usernameOrEmail string
|
||||
isEmail bool
|
||||
password string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "包含@符号,识别为邮箱",
|
||||
usernameOrEmail: "user@example.com",
|
||||
isEmail: true,
|
||||
name: "用户名登录成功",
|
||||
usernameOrEmail: "testlogin",
|
||||
password: "password123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "不包含@符号,识别为用户名",
|
||||
usernameOrEmail: "username",
|
||||
isEmail: false,
|
||||
name: "邮箱登录成功",
|
||||
usernameOrEmail: "login@example.com",
|
||||
password: "password123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "空字符串",
|
||||
usernameOrEmail: "",
|
||||
isEmail: false,
|
||||
name: "密码错误",
|
||||
usernameOrEmail: "testlogin",
|
||||
password: "wrongpassword",
|
||||
wantErr: true,
|
||||
errMsg: "用户名/邮箱或密码错误",
|
||||
},
|
||||
{
|
||||
name: "只有@符号",
|
||||
usernameOrEmail: "@",
|
||||
isEmail: true,
|
||||
name: "用户不存在",
|
||||
usernameOrEmail: "nonexistent",
|
||||
password: "password123",
|
||||
wantErr: true,
|
||||
errMsg: "用户名/邮箱或密码错误",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isEmail := strings.Contains(tt.usernameOrEmail, "@")
|
||||
if isEmail != tt.isEmail {
|
||||
t.Errorf("Email detection failed: got %v, want %v", isEmail, tt.isEmail)
|
||||
user, token, err := userService.Login(tt.usernameOrEmail, tt.password, "127.0.0.1", "test-agent")
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("期望返回错误,但实际没有错误")
|
||||
} else if tt.errMsg != "" && err.Error() != tt.errMsg {
|
||||
t.Errorf("错误信息不匹配: got %v, want %v", err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("不期望返回错误: %v", err)
|
||||
}
|
||||
if user == nil {
|
||||
t.Error("用户不应为nil")
|
||||
}
|
||||
if token == "" {
|
||||
t.Error("Token不应为空")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserService_Constants 测试用户服务相关常量
|
||||
func TestUserService_Constants(t *testing.T) {
|
||||
// 测试默认用户角色
|
||||
defaultRole := "user"
|
||||
if defaultRole == "" {
|
||||
t.Error("默认用户角色不能为空")
|
||||
// TestUserServiceImpl_BasicGetters 测试 GetByID / GetByEmail / UpdateInfo / UpdateAvatar
|
||||
func TestUserServiceImpl_BasicGettersAndUpdates(t *testing.T) {
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
// 预置用户
|
||||
user := &model.User{
|
||||
ID: 1,
|
||||
Username: "basic",
|
||||
Email: "basic@example.com",
|
||||
Avatar: "",
|
||||
}
|
||||
userRepo.Create(user)
|
||||
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
|
||||
// GetByID
|
||||
gotByID, err := userService.GetByID(1)
|
||||
if err != nil || gotByID == nil || gotByID.ID != 1 {
|
||||
t.Fatalf("GetByID 返回不正确: user=%+v, err=%v", gotByID, err)
|
||||
}
|
||||
|
||||
// 测试默认用户状态
|
||||
defaultStatus := int16(1)
|
||||
if defaultStatus != 1 {
|
||||
t.Errorf("默认用户状态应为1(正常),实际为%d", defaultStatus)
|
||||
// GetByEmail
|
||||
gotByEmail, err := userService.GetByEmail("basic@example.com")
|
||||
if err != nil || gotByEmail == nil || gotByEmail.Email != "basic@example.com" {
|
||||
t.Fatalf("GetByEmail 返回不正确: user=%+v, err=%v", gotByEmail, err)
|
||||
}
|
||||
|
||||
// 测试初始积分
|
||||
initialPoints := 0
|
||||
if initialPoints < 0 {
|
||||
t.Errorf("初始积分不应为负数,实际为%d", initialPoints)
|
||||
// UpdateInfo
|
||||
user.Username = "updated"
|
||||
if err := userService.UpdateInfo(user); err != nil {
|
||||
t.Fatalf("UpdateInfo 失败: %v", err)
|
||||
}
|
||||
updated, _ := userRepo.FindByID(1)
|
||||
if updated.Username != "updated" {
|
||||
t.Fatalf("UpdateInfo 未更新用户名, got=%s", updated.Username)
|
||||
}
|
||||
|
||||
// UpdateAvatar 只需确认不会返回错误(具体字段更新由仓库层保证)
|
||||
if err := userService.UpdateAvatar(1, "http://example.com/avatar.png"); err != nil {
|
||||
t.Fatalf("UpdateAvatar 失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserService_Validation 测试用户数据验证逻辑
|
||||
func TestUserService_Validation(t *testing.T) {
|
||||
// TestUserServiceImpl_ChangePassword 测试 ChangePassword
|
||||
func TestUserServiceImpl_ChangePassword(t *testing.T) {
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
hashed, _ := auth.HashPassword("oldpass")
|
||||
user := &model.User{
|
||||
ID: 1,
|
||||
Username: "changepw",
|
||||
Password: hashed,
|
||||
}
|
||||
userRepo.Create(user)
|
||||
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
|
||||
// 原密码正确
|
||||
if err := userService.ChangePassword(1, "oldpass", "newpass"); err != nil {
|
||||
t.Fatalf("ChangePassword 正常情况失败: %v", err)
|
||||
}
|
||||
|
||||
// 用户不存在
|
||||
if err := userService.ChangePassword(999, "oldpass", "newpass"); err == nil {
|
||||
t.Fatalf("ChangePassword 应在用户不存在时返回错误")
|
||||
}
|
||||
|
||||
// 原密码错误
|
||||
if err := userService.ChangePassword(1, "wrong", "another"); err == nil {
|
||||
t.Fatalf("ChangePassword 应在原密码错误时返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserServiceImpl_ResetPassword 测试 ResetPassword
|
||||
func TestUserServiceImpl_ResetPassword(t *testing.T) {
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
user := &model.User{
|
||||
ID: 1,
|
||||
Username: "resetpw",
|
||||
Email: "reset@example.com",
|
||||
}
|
||||
userRepo.Create(user)
|
||||
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
|
||||
// 正常重置
|
||||
if err := userService.ResetPassword("reset@example.com", "newpass"); err != nil {
|
||||
t.Fatalf("ResetPassword 正常情况失败: %v", err)
|
||||
}
|
||||
|
||||
// 用户不存在
|
||||
if err := userService.ResetPassword("notfound@example.com", "newpass"); err == nil {
|
||||
t.Fatalf("ResetPassword 应在用户不存在时返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserServiceImpl_ChangeEmail 测试 ChangeEmail
|
||||
func TestUserServiceImpl_ChangeEmail(t *testing.T) {
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
user1 := &model.User{ID: 1, Email: "user1@example.com"}
|
||||
user2 := &model.User{ID: 2, Email: "user2@example.com"}
|
||||
userRepo.Create(user1)
|
||||
userRepo.Create(user2)
|
||||
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
|
||||
// 正常修改
|
||||
if err := userService.ChangeEmail(1, "new@example.com"); err != nil {
|
||||
t.Fatalf("ChangeEmail 正常情况失败: %v", err)
|
||||
}
|
||||
|
||||
// 邮箱被其他用户占用
|
||||
if err := userService.ChangeEmail(1, "user2@example.com"); err == nil {
|
||||
t.Fatalf("ChangeEmail 应在邮箱被占用时返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserServiceImpl_ValidateAvatarURL 测试 ValidateAvatarURL
|
||||
func TestUserServiceImpl_ValidateAvatarURL(t *testing.T) {
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
email string
|
||||
password string
|
||||
wantValid bool
|
||||
name string
|
||||
url string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "有效的用户名和邮箱",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "用户名为空",
|
||||
username: "",
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "邮箱为空",
|
||||
username: "testuser",
|
||||
email: "",
|
||||
password: "password123",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "密码为空",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
password: "",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "邮箱格式无效(缺少@)",
|
||||
username: "testuser",
|
||||
email: "invalid-email",
|
||||
password: "password123",
|
||||
wantValid: false,
|
||||
},
|
||||
{"空字符串通过", "", false},
|
||||
{"相对路径通过", "/images/avatar.png", false},
|
||||
{"非法URL格式", "://bad-url", true},
|
||||
{"非法协议", "ftp://example.com/avatar.png", true},
|
||||
{"缺少主机名", "http:///avatar.png", true},
|
||||
{"本地域名通过", "http://localhost/avatar.png", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 简单的验证逻辑测试
|
||||
isValid := tt.username != "" && tt.email != "" && tt.password != "" && strings.Contains(tt.email, "@")
|
||||
if isValid != tt.wantValid {
|
||||
t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid)
|
||||
err := userService.ValidateAvatarURL(tt.url)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("ValidateAvatarURL(%q) error = %v, wantErr=%v", tt.url, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserService_AvatarLogic 测试头像逻辑
|
||||
func TestUserService_AvatarLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
providedAvatar string
|
||||
defaultAvatar string
|
||||
expectedAvatar string
|
||||
}{
|
||||
{
|
||||
name: "提供头像时使用提供的头像",
|
||||
providedAvatar: "https://example.com/custom.png",
|
||||
defaultAvatar: "https://example.com/default.png",
|
||||
expectedAvatar: "https://example.com/custom.png",
|
||||
},
|
||||
{
|
||||
name: "未提供头像时使用默认头像",
|
||||
providedAvatar: "",
|
||||
defaultAvatar: "https://example.com/default.png",
|
||||
expectedAvatar: "https://example.com/default.png",
|
||||
},
|
||||
// TestUserServiceImpl_MaxLimits 测试 GetMaxProfilesPerUser / GetMaxTexturesPerUser
|
||||
func TestUserServiceImpl_MaxLimits(t *testing.T) {
|
||||
userRepo := NewMockUserRepository()
|
||||
configRepo := NewMockSystemConfigRepository()
|
||||
jwtService := auth.NewJWTService("secret", 1)
|
||||
logger := zap.NewNop()
|
||||
|
||||
// 未配置时走默认值
|
||||
userService := NewUserService(userRepo, configRepo, jwtService, nil, logger)
|
||||
if got := userService.GetMaxProfilesPerUser(); got != 5 {
|
||||
t.Fatalf("GetMaxProfilesPerUser 默认值错误, got=%d", got)
|
||||
}
|
||||
if got := userService.GetMaxTexturesPerUser(); got != 50 {
|
||||
t.Fatalf("GetMaxTexturesPerUser 默认值错误, got=%d", got)
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
avatarURL := tt.providedAvatar
|
||||
if avatarURL == "" {
|
||||
avatarURL = tt.defaultAvatar
|
||||
}
|
||||
if avatarURL != tt.expectedAvatar {
|
||||
t.Errorf("Avatar logic failed: got %s, want %s", avatarURL, tt.expectedAvatar)
|
||||
}
|
||||
})
|
||||
// 配置有效值
|
||||
configRepo.Update(&model.SystemConfig{Key: "max_profiles_per_user", Value: "10"})
|
||||
configRepo.Update(&model.SystemConfig{Key: "max_textures_per_user", Value: "100"})
|
||||
|
||||
if got := userService.GetMaxProfilesPerUser(); got != 10 {
|
||||
t.Fatalf("GetMaxProfilesPerUser 配置值错误, got=%d", got)
|
||||
}
|
||||
}
|
||||
if got := userService.GetMaxTexturesPerUser(); got != 100 {
|
||||
t.Fatalf("GetMaxTexturesPerUser 配置值错误, got=%d", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user