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:
lafay
2025-12-02 19:43:39 +08:00
parent 188a05caa7
commit 801f1b1397
33 changed files with 3628 additions and 4129 deletions

View File

@@ -1,9 +1,13 @@
package service
import (
"context"
"errors"
"strings"
"testing"
"time"
"carrotskin/pkg/storage"
)
// TestUploadService_FileTypes 测试文件类型常量
@@ -135,43 +139,43 @@ func TestGetUploadConfig_TextureConfig(t *testing.T) {
// TestValidateFileName 测试文件名验证
func TestValidateFileName(t *testing.T) {
tests := []struct {
name string
fileName string
fileType FileType
wantErr bool
name string
fileName string
fileType FileType
wantErr bool
errContains string
}{
{
name: "有效的头像文件名",
fileName: "avatar.png",
fileType: FileTypeAvatar,
wantErr: false,
name: "有效的头像文件名",
fileName: "avatar.png",
fileType: FileTypeAvatar,
wantErr: false,
},
{
name: "有效的材质文件名",
fileName: "texture.png",
fileType: FileTypeTexture,
wantErr: false,
name: "有效的材质文件名",
fileName: "texture.png",
fileType: FileTypeTexture,
wantErr: false,
},
{
name: "文件名为空",
fileName: "",
fileType: FileTypeAvatar,
wantErr: true,
name: "文件名为空",
fileName: "",
fileType: FileTypeAvatar,
wantErr: true,
errContains: "文件名不能为空",
},
{
name: "不支持的文件扩展名",
fileName: "file.txt",
fileType: FileTypeAvatar,
wantErr: true,
name: "不支持的文件扩展名",
fileName: "file.txt",
fileType: FileTypeAvatar,
wantErr: true,
errContains: "不支持的文件格式",
},
{
name: "无效的文件类型",
fileName: "file.png",
fileType: FileType("invalid"),
wantErr: true,
name: "无效的文件类型",
fileName: "file.png",
fileType: FileType("invalid"),
wantErr: true,
errContains: "不支持的文件类型",
},
}
@@ -277,3 +281,130 @@ func TestUploadConfig_Structure(t *testing.T) {
}
}
// mockStorageClient 用于单元测试的简单存储客户端假实现
// 注意:这里只声明与 upload_service 使用到的方法,避免依赖真实 MinIO 客户端
type mockStorageClient struct {
getBucketFn func(name string) (string, error)
generatePresignedPostURLFn func(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration) (*storage.PresignedPostPolicyResult, error)
}
func (m *mockStorageClient) GetBucket(name string) (string, error) {
if m.getBucketFn != nil {
return m.getBucketFn(name)
}
return "", errors.New("GetBucket not implemented")
}
func (m *mockStorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration) (*storage.PresignedPostPolicyResult, error) {
if m.generatePresignedPostURLFn != nil {
return m.generatePresignedPostURLFn(ctx, bucketName, objectName, minSize, maxSize, expires)
}
return nil, errors.New("GeneratePresignedPostURL not implemented")
}
// TestGenerateAvatarUploadURL_Success 测试头像上传URL生成成功
func TestGenerateAvatarUploadURL_Success(t *testing.T) {
ctx := context.Background()
mockClient := &mockStorageClient{
getBucketFn: func(name string) (string, error) {
if name != "avatars" {
t.Fatalf("unexpected bucket name: %s", name)
}
return "avatars-bucket", nil
},
generatePresignedPostURLFn: func(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration) (*storage.PresignedPostPolicyResult, error) {
if bucketName != "avatars-bucket" {
t.Fatalf("unexpected bucketName: %s", bucketName)
}
if !strings.Contains(objectName, "user_") {
t.Fatalf("objectName should contain user_ prefix, got: %s", objectName)
}
if !strings.Contains(objectName, "avatar.png") {
t.Fatalf("objectName should contain original file name, got: %s", objectName)
}
// 检查大小与过期时间传递
if minSize != 1024 {
t.Fatalf("minSize = %d, want 1024", minSize)
}
if maxSize != 5*1024*1024 {
t.Fatalf("maxSize = %d, want 5MB", maxSize)
}
if expires != 15*time.Minute {
t.Fatalf("expires = %v, want 15m", expires)
}
return &storage.PresignedPostPolicyResult{
PostURL: "http://example.com/upload",
FormData: map[string]string{"key": objectName},
FileURL: "http://example.com/file/" + objectName,
}, nil
},
}
// 直接将 mock 实例转换为真实类型使用(依赖其方法集与被测代码一致)
storageClient := (*storage.StorageClient)(nil)
_ = storageClient // 避免未使用告警,实际调用仍通过 mockClient 完成
// 直接通过内部使用接口的实现进行测试,避免依赖真实 StorageClient
result, err := generateAvatarUploadURLWithClient(ctx, mockClient, 123, "avatar.png")
if err != nil {
t.Fatalf("GenerateAvatarUploadURL() error = %v, want nil", err)
}
if result == nil {
t.Fatalf("GenerateAvatarUploadURL() result is nil")
}
if result.PostURL == "" || result.FileURL == "" {
t.Fatalf("GenerateAvatarUploadURL() result has empty URLs: %+v", result)
}
}
// TestGenerateTextureUploadURL_Success 测试材质上传URL生成成功SKIN/CAPE
func TestGenerateTextureUploadURL_Success(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
textureType string
}{
{"SKIN 材质", "SKIN"},
{"CAPE 材质", "CAPE"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &mockStorageClient{
getBucketFn: func(name string) (string, error) {
if name != "textures" {
t.Fatalf("unexpected bucket name: %s", name)
}
return "textures-bucket", nil
},
generatePresignedPostURLFn: func(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration) (*storage.PresignedPostPolicyResult, error) {
if bucketName != "textures-bucket" {
t.Fatalf("unexpected bucketName: %s", bucketName)
}
if !strings.Contains(objectName, "texture.png") {
t.Fatalf("objectName should contain original file name, got: %s", objectName)
}
if !strings.Contains(objectName, "/"+strings.ToLower(tt.textureType)+"/") {
t.Fatalf("objectName should contain texture type folder, got: %s", objectName)
}
return &storage.PresignedPostPolicyResult{
PostURL: "http://example.com/upload",
FormData: map[string]string{"key": objectName},
FileURL: "http://example.com/file/" + objectName,
}, nil
},
}
result, err := generateTextureUploadURLWithClient(ctx, mockClient, 123, "texture.png", tt.textureType)
if err != nil {
t.Fatalf("generateTextureUploadURLWithClient() error = %v, want nil", err)
}
if result == nil || result.PostURL == "" || result.FileURL == "" {
t.Fatalf("generateTextureUploadURLWithClient() result invalid: %+v", result)
}
})
}
}