package service import ( "context" "errors" "strings" "testing" "time" "carrotskin/pkg/storage" ) // TestUploadService_FileTypes 测试文件类型常量 func TestUploadService_FileTypes(t *testing.T) { if FileTypeAvatar == "" { t.Error("FileTypeAvatar should not be empty") } if FileTypeTexture == "" { t.Error("FileTypeTexture should not be empty") } if FileTypeAvatar == FileTypeTexture { t.Error("FileTypeAvatar and FileTypeTexture should be different") } } // TestGetUploadConfig 测试获取上传配置 func TestGetUploadConfig(t *testing.T) { tests := []struct { name string fileType FileType wantConfig bool }{ { name: "头像类型返回配置", fileType: FileTypeAvatar, wantConfig: true, }, { name: "材质类型返回配置", fileType: FileTypeTexture, wantConfig: true, }, { name: "无效类型返回nil", fileType: FileType("invalid"), wantConfig: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := GetUploadConfig(tt.fileType) hasConfig := config != nil if hasConfig != tt.wantConfig { t.Errorf("GetUploadConfig() = %v, want %v", hasConfig, tt.wantConfig) } if config != nil { // 验证配置字段 if config.MinSize <= 0 { t.Error("MinSize should be greater than 0") } if config.MaxSize <= 0 { t.Error("MaxSize should be greater than 0") } if config.MaxSize < config.MinSize { t.Error("MaxSize should be greater than or equal to MinSize") } if config.Expires <= 0 { t.Error("Expires should be greater than 0") } if len(config.AllowedExts) == 0 { t.Error("AllowedExts should not be empty") } } }) } } // TestGetUploadConfig_AvatarConfig 测试头像配置详情 func TestGetUploadConfig_AvatarConfig(t *testing.T) { config := GetUploadConfig(FileTypeAvatar) if config == nil { t.Fatal("Avatar config should not be nil") } // 验证允许的扩展名 expectedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} for _, ext := range expectedExts { if !config.AllowedExts[ext] { t.Errorf("Avatar config should allow %s extension", ext) } } // 验证文件大小限制 if config.MinSize != 1024 { t.Errorf("Avatar MinSize = %d, want 1024", config.MinSize) } if config.MaxSize != 5*1024*1024 { t.Errorf("Avatar MaxSize = %d, want 5MB", config.MaxSize) } // 验证过期时间 if config.Expires != 15*time.Minute { t.Errorf("Avatar Expires = %v, want 15 minutes", config.Expires) } } // TestGetUploadConfig_TextureConfig 测试材质配置详情 func TestGetUploadConfig_TextureConfig(t *testing.T) { config := GetUploadConfig(FileTypeTexture) if config == nil { t.Fatal("Texture config should not be nil") } // 验证允许的扩展名(材质只允许PNG) if !config.AllowedExts[".png"] { t.Error("Texture config should allow .png extension") } // 验证文件大小限制 if config.MinSize != 1024 { t.Errorf("Texture MinSize = %d, want 1024", config.MinSize) } if config.MaxSize != 10*1024*1024 { t.Errorf("Texture MaxSize = %d, want 10MB", config.MaxSize) } // 验证过期时间 if config.Expires != 15*time.Minute { t.Errorf("Texture Expires = %v, want 15 minutes", config.Expires) } } // TestValidateFileName 测试文件名验证 func TestValidateFileName(t *testing.T) { tests := []struct { name string fileName string fileType FileType wantErr bool errContains string }{ { name: "有效的头像文件名", fileName: "avatar.png", fileType: FileTypeAvatar, wantErr: false, }, { name: "有效的材质文件名", fileName: "texture.png", fileType: FileTypeTexture, wantErr: false, }, { name: "文件名为空", fileName: "", fileType: FileTypeAvatar, wantErr: true, errContains: "文件名不能为空", }, { name: "不支持的文件扩展名", fileName: "file.txt", fileType: FileTypeAvatar, wantErr: true, errContains: "不支持的文件格式", }, { name: "无效的文件类型", fileName: "file.png", fileType: FileType("invalid"), wantErr: true, errContains: "不支持的文件类型", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateFileName(tt.fileName, tt.fileType) if (err != nil) != tt.wantErr { t.Errorf("ValidateFileName() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr && tt.errContains != "" { if err == nil || !strings.Contains(err.Error(), tt.errContains) { t.Errorf("ValidateFileName() error = %v, should contain %s", err, tt.errContains) } } }) } } // TestValidateFileName_Extensions 测试各种扩展名 func TestValidateFileName_Extensions(t *testing.T) { avatarExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} for _, ext := range avatarExts { fileName := "test" + ext err := ValidateFileName(fileName, FileTypeAvatar) if err != nil { t.Errorf("Avatar file with %s extension should be valid, got error: %v", ext, err) } } // 材质只支持PNG textureExts := []string{".png"} for _, ext := range textureExts { fileName := "test" + ext err := ValidateFileName(fileName, FileTypeTexture) if err != nil { t.Errorf("Texture file with %s extension should be valid, got error: %v", ext, err) } } // 测试不支持的扩展名 invalidExts := []string{".txt", ".pdf", ".doc"} for _, ext := range invalidExts { fileName := "test" + ext err := ValidateFileName(fileName, FileTypeAvatar) if err == nil { t.Errorf("Avatar file with %s extension should be invalid", ext) } } } // TestValidateFileName_CaseInsensitive 测试扩展名大小写不敏感 func TestValidateFileName_CaseInsensitive(t *testing.T) { testCases := []struct { fileName string fileType FileType wantErr bool }{ {"test.PNG", FileTypeAvatar, false}, {"test.JPG", FileTypeAvatar, false}, {"test.JPEG", FileTypeAvatar, false}, {"test.GIF", FileTypeAvatar, false}, {"test.WEBP", FileTypeAvatar, false}, {"test.PnG", FileTypeTexture, false}, } for _, tc := range testCases { t.Run(tc.fileName, func(t *testing.T) { err := ValidateFileName(tc.fileName, tc.fileType) if (err != nil) != tc.wantErr { t.Errorf("ValidateFileName(%s, %s) error = %v, wantErr %v", tc.fileName, tc.fileType, err, tc.wantErr) } }) } } // TestUploadConfig_Structure 测试UploadConfig结构 func TestUploadConfig_Structure(t *testing.T) { config := &UploadConfig{ AllowedExts: map[string]bool{ ".png": true, }, MinSize: 1024, MaxSize: 5 * 1024 * 1024, Expires: 15 * time.Minute, } if config.AllowedExts == nil { t.Error("AllowedExts should not be nil") } if config.MinSize <= 0 { t.Error("MinSize should be greater than 0") } if config.MaxSize <= config.MinSize { t.Error("MaxSize should be greater than MinSize") } if config.Expires <= 0 { t.Error("Expires should be greater than 0") } } // 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) } }) } }