按 MinecraftServices 文档实现好友系四组接口与 profiles 服务:
好友系统(/api/yggdrasil/minecraftservices/*)
- Friends(1.3):列表查询、ADD/REMOVE 操作(发请求/接受/拒绝/撤回/删除)
- 状态枚举+单向记录模型,接受请求时合并两方向避免好友列表重复
- Player Attributes(1.2):friendsPreferences / 脏词过滤 / 聊天偏好读写
- Upsert 用 map 显式写字段规避 gorm 对带 default 零值布尔字段的忽略
- Presence(1.1):Redis key+TTL 上报与好友在线状态批量查询
- Blocklist(1.6):屏蔽列表查询(含 120s Redis 缓存),Block/Unblock 内部方法
Profiles 服务
- getManyByName(2.1):POST /api/profiles/minecraft,返回 [{id,name}]
- toLowerCase 规范化、去重、空名过滤、maxBatch=10 超限拒绝
- getByName(2.2):GET /api/users/profiles/minecraft/:name,返回 {id,name},未找到返回 404
路由与基础设施
- 路由按官方 host 前缀一一转发
- api.mojang.com/* -> /api/yggdrasil/api/*
- api.minecraftservices.com/* -> /api/yggdrasil/minecraftservices/*
- 新增 Friend/PlayerAttributes 模型与 AutoMigrate 注册
- 新增 FriendsService 与 Container 装配
- 抽 extractBearerToken helper,统一 MinecraftServices 错误响应
修复
- texture_service: ToggleFavorite 在 db 为 nil 时降级非事务执行
修复 TestTextureServiceImpl_ToggleFavorite 空指针 panic
- texture_service_test: UploadTexture 用例改用真实 SHA-256 命中 mock
修复 4 个子用例因文件大小/Hash 不匹配的失败
- profile_repository: GetByNames/FindByName 改 LOWER() 大小写不敏感
与客户端 toLowerCase 规范化对齐
测试
- 好友模型、ADD/REMOVE/接受、属性、屏蔽、Bearer 解析单测全绿
- profiles 查询规范化、去重、超限、大小写不敏感单测全绿
- go build ./... && go test ./... 全部通过
858 lines
20 KiB
Go
858 lines
20 KiB
Go
package service
|
||
|
||
import (
|
||
"carrotskin/internal/model"
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"testing"
|
||
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// sha256Hex 计算给定数据的 SHA-256 十六进制字符串
|
||
func sha256Hex(data []byte) string {
|
||
sum := sha256.Sum256(data)
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
// TestTextureService_TypeValidation 测试材质类型验证
|
||
func TestTextureService_TypeValidation(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
textureType string
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "SKIN类型有效",
|
||
textureType: "SKIN",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "CAPE类型有效",
|
||
textureType: "CAPE",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "无效类型",
|
||
textureType: "INVALID",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "空类型无效",
|
||
textureType: "",
|
||
wantValid: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
isValid := tt.textureType == "SKIN" || tt.textureType == "CAPE"
|
||
if isValid != tt.wantValid {
|
||
t.Errorf("Texture type validation failed: got %v, want %v", isValid, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestTextureService_DefaultValues 测试材质默认值
|
||
func TestTextureService_DefaultValues(t *testing.T) {
|
||
// 测试默认状态
|
||
defaultStatus := 1
|
||
if defaultStatus != 1 {
|
||
t.Errorf("默认状态应为1,实际为%d", defaultStatus)
|
||
}
|
||
|
||
// 测试默认下载数
|
||
defaultDownloadCount := 0
|
||
if defaultDownloadCount != 0 {
|
||
t.Errorf("默认下载数应为0,实际为%d", defaultDownloadCount)
|
||
}
|
||
|
||
// 测试默认收藏数
|
||
defaultFavoriteCount := 0
|
||
if defaultFavoriteCount != 0 {
|
||
t.Errorf("默认收藏数应为0,实际为%d", defaultFavoriteCount)
|
||
}
|
||
}
|
||
|
||
// TestTextureService_StatusValidation 测试材质状态验证
|
||
func TestTextureService_StatusValidation(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
status int16
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "状态为1(正常)时有效",
|
||
status: 1,
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "状态为-1(删除)时无效",
|
||
status: -1,
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "状态为0时可能有效(取决于业务逻辑)",
|
||
status: 0,
|
||
wantValid: true, // 状态为0(禁用)时,材质仍然存在,只是不可用,但查询时不会返回错误
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
// 材质状态为-1时表示已删除,无效
|
||
isValid := tt.status != -1
|
||
if isValid != tt.wantValid {
|
||
t.Errorf("Status validation failed: got %v, want %v", isValid, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGetUserTextures_Pagination 测试分页逻辑
|
||
func TestGetUserTextures_Pagination(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
page int
|
||
pageSize int
|
||
wantPage int
|
||
wantSize int
|
||
}{
|
||
{
|
||
name: "有效的分页参数",
|
||
page: 2,
|
||
pageSize: 20,
|
||
wantPage: 2,
|
||
wantSize: 20,
|
||
},
|
||
{
|
||
name: "page小于1,应该设为1",
|
||
page: 0,
|
||
pageSize: 20,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
{
|
||
name: "pageSize小于1,应该设为20",
|
||
page: 1,
|
||
pageSize: 0,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
{
|
||
name: "pageSize超过100,应该设为20",
|
||
page: 1,
|
||
pageSize: 200,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
page := tt.page
|
||
pageSize := tt.pageSize
|
||
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
|
||
if page != tt.wantPage {
|
||
t.Errorf("Page = %d, want %d", page, tt.wantPage)
|
||
}
|
||
if pageSize != tt.wantSize {
|
||
t.Errorf("PageSize = %d, want %d", pageSize, tt.wantSize)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestSearchTextures_Pagination 测试搜索分页逻辑
|
||
func TestSearchTextures_Pagination(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
page int
|
||
pageSize int
|
||
wantPage int
|
||
wantSize int
|
||
}{
|
||
{
|
||
name: "有效的分页参数",
|
||
page: 1,
|
||
pageSize: 10,
|
||
wantPage: 1,
|
||
wantSize: 10,
|
||
},
|
||
{
|
||
name: "page小于1,应该设为1",
|
||
page: -1,
|
||
pageSize: 20,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
{
|
||
name: "pageSize超过100,应该设为20",
|
||
page: 1,
|
||
pageSize: 150,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
page := tt.page
|
||
pageSize := tt.pageSize
|
||
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
|
||
if page != tt.wantPage {
|
||
t.Errorf("Page = %d, want %d", page, tt.wantPage)
|
||
}
|
||
if pageSize != tt.wantSize {
|
||
t.Errorf("PageSize = %d, want %d", pageSize, tt.wantSize)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestUpdateTexture_PermissionCheck 测试更新材质的权限检查
|
||
func TestUpdateTexture_PermissionCheck(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
uploaderID int64
|
||
requestID int64
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "上传者ID匹配,允许更新",
|
||
uploaderID: 1,
|
||
requestID: 1,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "上传者ID不匹配,拒绝更新",
|
||
uploaderID: 1,
|
||
requestID: 2,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hasError := tt.uploaderID != tt.requestID
|
||
if hasError != tt.wantErr {
|
||
t.Errorf("Permission check failed: got %v, want %v", hasError, tt.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestUpdateTexture_FieldUpdates 测试更新字段逻辑
|
||
func TestUpdateTexture_FieldUpdates(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
nameValue string
|
||
descValue string
|
||
isPublic *bool
|
||
wantUpdates int
|
||
}{
|
||
{
|
||
name: "更新所有字段",
|
||
nameValue: "NewName",
|
||
descValue: "NewDesc",
|
||
isPublic: boolPtr(true),
|
||
wantUpdates: 3,
|
||
},
|
||
{
|
||
name: "只更新名称",
|
||
nameValue: "NewName",
|
||
descValue: "",
|
||
isPublic: nil,
|
||
wantUpdates: 1,
|
||
},
|
||
{
|
||
name: "只更新描述",
|
||
nameValue: "",
|
||
descValue: "NewDesc",
|
||
isPublic: nil,
|
||
wantUpdates: 1,
|
||
},
|
||
{
|
||
name: "只更新公开状态",
|
||
nameValue: "",
|
||
descValue: "",
|
||
isPublic: boolPtr(false),
|
||
wantUpdates: 1,
|
||
},
|
||
{
|
||
name: "没有更新",
|
||
nameValue: "",
|
||
descValue: "",
|
||
isPublic: nil,
|
||
wantUpdates: 0,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
updates := 0
|
||
if tt.nameValue != "" {
|
||
updates++
|
||
}
|
||
if tt.descValue != "" {
|
||
updates++
|
||
}
|
||
if tt.isPublic != nil {
|
||
updates++
|
||
}
|
||
|
||
if updates != tt.wantUpdates {
|
||
t.Errorf("Updates count = %d, want %d", updates, tt.wantUpdates)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestDeleteTexture_PermissionCheck 测试删除材质的权限检查
|
||
func TestDeleteTexture_PermissionCheck(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
uploaderID int64
|
||
requestID int64
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "上传者ID匹配,允许删除",
|
||
uploaderID: 1,
|
||
requestID: 1,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "上传者ID不匹配,拒绝删除",
|
||
uploaderID: 1,
|
||
requestID: 2,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hasError := tt.uploaderID != tt.requestID
|
||
if hasError != tt.wantErr {
|
||
t.Errorf("Permission check failed: got %v, want %v", hasError, tt.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestToggleTextureFavorite_Logic 测试切换收藏状态的逻辑
|
||
func TestToggleTextureFavorite_Logic(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
isFavorited bool
|
||
wantResult bool
|
||
}{
|
||
{
|
||
name: "已收藏,取消收藏",
|
||
isFavorited: true,
|
||
wantResult: false,
|
||
},
|
||
{
|
||
name: "未收藏,添加收藏",
|
||
isFavorited: false,
|
||
wantResult: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result := !tt.isFavorited
|
||
if result != tt.wantResult {
|
||
t.Errorf("Toggle favorite failed: got %v, want %v", result, tt.wantResult)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGetUserTextureFavorites_Pagination 测试收藏列表分页
|
||
func TestGetUserTextureFavorites_Pagination(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
page int
|
||
pageSize int
|
||
wantPage int
|
||
wantSize int
|
||
}{
|
||
{
|
||
name: "有效的分页参数",
|
||
page: 1,
|
||
pageSize: 20,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
{
|
||
name: "page小于1,应该设为1",
|
||
page: 0,
|
||
pageSize: 20,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
{
|
||
name: "pageSize超过100,应该设为20",
|
||
page: 1,
|
||
pageSize: 200,
|
||
wantPage: 1,
|
||
wantSize: 20,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
page := tt.page
|
||
pageSize := tt.pageSize
|
||
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
|
||
if page != tt.wantPage {
|
||
t.Errorf("Page = %d, want %d", page, tt.wantPage)
|
||
}
|
||
if pageSize != tt.wantSize {
|
||
t.Errorf("PageSize = %d, want %d", pageSize, tt.wantSize)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestCheckTextureUploadLimit_Logic 测试上传限制检查逻辑
|
||
func TestCheckTextureUploadLimit_Logic(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
count int64
|
||
maxTextures int
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "未达到上限",
|
||
count: 5,
|
||
maxTextures: 10,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "达到上限",
|
||
count: 10,
|
||
maxTextures: 10,
|
||
wantErr: true,
|
||
},
|
||
{
|
||
name: "超过上限",
|
||
count: 15,
|
||
maxTextures: 10,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hasError := tt.count >= int64(tt.maxTextures)
|
||
if hasError != tt.wantErr {
|
||
t.Errorf("Limit check failed: got %v, want %v", hasError, tt.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 辅助函数
|
||
func boolPtr(b bool) *bool {
|
||
return &b
|
||
}
|
||
|
||
// ============================================================================
|
||
// 使用 Mock 的集成测试
|
||
// ============================================================================
|
||
|
||
// TestTextureServiceImpl_Create 测试创建Texture
|
||
func TestTextureServiceImpl_Create(t *testing.T) {
|
||
textureRepo := NewMockTextureRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 预置用户
|
||
testUser := &model.User{
|
||
ID: 1,
|
||
Username: "testuser",
|
||
Email: "test@example.com",
|
||
Status: 1,
|
||
}
|
||
_ = userRepo.Create(context.Background(), testUser)
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||
|
||
// 构造一份符合 service 大小校验(>=512B)的 PNG 文件数据
|
||
pngFileData := make([]byte, 1024)
|
||
for i := range pngFileData {
|
||
pngFileData[i] = byte(i % 256)
|
||
}
|
||
// 计算其真实 SHA-256,用于 mock 预置与命中分支测试
|
||
existingHash := sha256Hex(pngFileData)
|
||
_ = textureRepo.Create(context.Background(), &model.Texture{
|
||
ID: 100,
|
||
UploaderID: 1,
|
||
Name: "ExistingTexture",
|
||
Hash: existingHash,
|
||
URL: "https://example.com/existing.png",
|
||
})
|
||
|
||
tests := []struct {
|
||
name string
|
||
uploaderID int64
|
||
textureName string
|
||
textureType string
|
||
fileData []byte
|
||
wantErr bool
|
||
errContains string
|
||
}{
|
||
{
|
||
name: " Hash 已存在 -> 复用 URL",
|
||
uploaderID: 1,
|
||
textureName: "TestSkin",
|
||
textureType: "SKIN",
|
||
fileData: pngFileData, // hash 命中预置记录
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "Hash 不存在且 storage 为 nil -> 存储不可用",
|
||
uploaderID: 1,
|
||
textureName: "NewCape",
|
||
textureType: "CAPE",
|
||
fileData: make([]byte, 1024), // 不同内容,hash 不会命中
|
||
wantErr: true,
|
||
errContains: "存储服务不可用",
|
||
},
|
||
{
|
||
name: "用户不存在",
|
||
uploaderID: 999,
|
||
textureName: "TestTexture",
|
||
textureType: "SKIN",
|
||
fileData: pngFileData,
|
||
wantErr: true,
|
||
},
|
||
{
|
||
name: "无效的材质类型",
|
||
uploaderID: 1,
|
||
textureName: "InvalidTypeTexture",
|
||
textureType: "INVALID",
|
||
fileData: pngFileData,
|
||
wantErr: true,
|
||
errContains: "无效的材质类型",
|
||
},
|
||
{
|
||
name: "文件过小",
|
||
uploaderID: 1,
|
||
textureName: "TooSmall",
|
||
textureType: "SKIN",
|
||
fileData: []byte("tiny"),
|
||
wantErr: true,
|
||
errContains: "文件大小必须在",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx := context.Background()
|
||
texture, err := textureService.UploadTexture(
|
||
ctx,
|
||
tt.uploaderID,
|
||
tt.textureName,
|
||
"Test description",
|
||
tt.textureType,
|
||
tt.fileData,
|
||
"test.png",
|
||
true,
|
||
false,
|
||
)
|
||
|
||
if tt.wantErr {
|
||
if err == nil {
|
||
t.Error("期望返回错误,但实际没有错误")
|
||
return
|
||
}
|
||
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
|
||
t.Errorf("错误信息应包含 %q, 实际为: %v", tt.errContains, err.Error())
|
||
}
|
||
return
|
||
}
|
||
if err != nil {
|
||
t.Errorf("不期望返回错误: %v", err)
|
||
return
|
||
}
|
||
if texture == nil {
|
||
t.Fatal("返回的Texture不应为nil")
|
||
}
|
||
if texture.Name != tt.textureName {
|
||
t.Errorf("Texture名称不匹配: got %v, want %v", texture.Name, tt.textureName)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestTextureServiceImpl_GetByID 测试获取Texture
|
||
func TestTextureServiceImpl_GetByID(t *testing.T) {
|
||
textureRepo := NewMockTextureRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 预置Texture
|
||
testTexture := &model.Texture{
|
||
ID: 1,
|
||
UploaderID: 1,
|
||
Name: "TestTexture",
|
||
Hash: "test-hash",
|
||
}
|
||
_ = textureRepo.Create(context.Background(), testTexture)
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
id int64
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "获取存在的Texture",
|
||
id: 1,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "获取不存在的Texture",
|
||
id: 999,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx := context.Background()
|
||
texture, err := textureService.GetByID(ctx, tt.id)
|
||
|
||
if tt.wantErr {
|
||
if err == nil {
|
||
t.Error("期望返回错误,但实际没有错误")
|
||
}
|
||
} else {
|
||
if err != nil {
|
||
t.Errorf("不期望返回错误: %v", err)
|
||
return
|
||
}
|
||
if texture == nil {
|
||
t.Error("返回的Texture不应为nil")
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestTextureServiceImpl_GetByUserID_And_Search 测试 GetByUserID 与 Search 分页封装
|
||
func TestTextureServiceImpl_GetByUserID_And_Search(t *testing.T) {
|
||
textureRepo := NewMockTextureRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 预置多条 Texture
|
||
for i := int64(1); i <= 5; i++ {
|
||
_ = textureRepo.Create(context.Background(), &model.Texture{
|
||
ID: i,
|
||
UploaderID: 1,
|
||
Name: "T",
|
||
IsPublic: i%2 == 0,
|
||
})
|
||
}
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||
|
||
ctx := context.Background()
|
||
|
||
// GetByUserID 应按上传者过滤并调用 NormalizePagination
|
||
textures, total, err := textureService.GetByUserID(ctx, 1, 0, 0)
|
||
if err != nil {
|
||
t.Fatalf("GetByUserID 失败: %v", err)
|
||
}
|
||
if total != int64(len(textures)) {
|
||
t.Fatalf("GetByUserID 返回数量与总数不一致, total=%d, len=%d", total, len(textures))
|
||
}
|
||
|
||
// Search 仅验证能够正常调用并返回结果
|
||
searchResult, searchTotal, err := textureService.Search(ctx, "", model.TextureTypeSkin, true, -1, 200)
|
||
if err != nil {
|
||
t.Fatalf("Search 失败: %v", err)
|
||
}
|
||
if searchTotal != int64(len(searchResult)) {
|
||
t.Fatalf("Search 返回数量与总数不一致, total=%d, len=%d", searchTotal, len(searchResult))
|
||
}
|
||
}
|
||
|
||
// TestTextureServiceImpl_Update_And_Delete 测试 Update / Delete 权限与字段更新
|
||
func TestTextureServiceImpl_Update_And_Delete(t *testing.T) {
|
||
textureRepo := NewMockTextureRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
texture := &model.Texture{
|
||
ID: 1,
|
||
UploaderID: 1,
|
||
Name: "Old",
|
||
Description: "OldDesc",
|
||
IsPublic: false,
|
||
}
|
||
_ = textureRepo.Create(context.Background(), texture)
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||
|
||
ctx := context.Background()
|
||
|
||
// 更新成功
|
||
newName := "NewName"
|
||
newDesc := "NewDesc"
|
||
public := boolPtr(true)
|
||
updated, err := textureService.Update(ctx, 1, 1, newName, newDesc, public)
|
||
if err != nil {
|
||
t.Fatalf("Update 正常情况失败: %v", err)
|
||
}
|
||
// 由于 MockTextureRepository.UpdateFields 不会真正修改结构体字段,这里只验证不会返回 nil 即可
|
||
if updated == nil {
|
||
t.Fatalf("Update 返回结果不应为 nil")
|
||
}
|
||
|
||
// 无权限更新
|
||
if _, err := textureService.Update(ctx, 1, 2, "X", "Y", nil); err == nil {
|
||
t.Fatalf("Update 在无权限时应返回错误")
|
||
}
|
||
|
||
// 删除成功
|
||
if err := textureService.Delete(ctx, 1, 1); err != nil {
|
||
t.Fatalf("Delete 正常情况失败: %v", err)
|
||
}
|
||
|
||
// 无权限删除
|
||
if err := textureService.Delete(ctx, 1, 2); err == nil {
|
||
t.Fatalf("Delete 在无权限时应返回错误")
|
||
}
|
||
}
|
||
|
||
// TestTextureServiceImpl_FavoritesAndLimit 测试 GetUserFavorites 与 CheckUploadLimit
|
||
func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) {
|
||
textureRepo := NewMockTextureRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 预置若干 Texture 与收藏关系
|
||
for i := int64(1); i <= 3; i++ {
|
||
_ = textureRepo.Create(context.Background(), &model.Texture{
|
||
ID: i,
|
||
UploaderID: 1,
|
||
Name: "T",
|
||
})
|
||
_ = textureRepo.AddFavorite(context.Background(), 1, i)
|
||
}
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||
|
||
ctx := context.Background()
|
||
|
||
// GetUserFavorites
|
||
favs, total, err := textureService.GetUserFavorites(ctx, 1, -1, -1)
|
||
if err != nil {
|
||
t.Fatalf("GetUserFavorites 失败: %v", err)
|
||
}
|
||
if int64(len(favs)) != total || total != 3 {
|
||
t.Fatalf("GetUserFavorites 数量不正确, total=%d, len=%d", total, len(favs))
|
||
}
|
||
|
||
// CheckUploadLimit 未超过上限
|
||
if err := textureService.CheckUploadLimit(ctx, 1, 10); err != nil {
|
||
t.Fatalf("CheckUploadLimit 在未达到上限时不应报错: %v", err)
|
||
}
|
||
|
||
// CheckUploadLimit 超过上限
|
||
if err := textureService.CheckUploadLimit(ctx, 1, 2); err == nil {
|
||
t.Fatalf("CheckUploadLimit 在超过上限时应返回错误")
|
||
}
|
||
}
|
||
|
||
// TestTextureServiceImpl_ToggleFavorite 测试收藏功能
|
||
func TestTextureServiceImpl_ToggleFavorite(t *testing.T) {
|
||
textureRepo := NewMockTextureRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 预置用户和Texture
|
||
testUser := &model.User{ID: 1, Username: "testuser", Status: 1}
|
||
_ = userRepo.Create(context.Background(), testUser)
|
||
|
||
testTexture := &model.Texture{
|
||
ID: 1,
|
||
UploaderID: 1,
|
||
Name: "TestTexture",
|
||
Hash: "test-hash",
|
||
}
|
||
_ = textureRepo.Create(context.Background(), testTexture)
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||
|
||
ctx := context.Background()
|
||
|
||
// 第一次收藏
|
||
isFavorited, err := textureService.ToggleFavorite(ctx, 1, 1)
|
||
if err != nil {
|
||
t.Errorf("第一次收藏失败: %v", err)
|
||
}
|
||
if !isFavorited {
|
||
t.Error("第一次操作应该是添加收藏")
|
||
}
|
||
|
||
// 第二次取消收藏
|
||
isFavorited, err = textureService.ToggleFavorite(ctx, 1, 1)
|
||
if err != nil {
|
||
t.Errorf("取消收藏失败: %v", err)
|
||
}
|
||
if isFavorited {
|
||
t.Error("第二次操作应该是取消收藏")
|
||
}
|
||
}
|
||
|
||
// 辅助函数
|
||
func containsString(s, substr string) bool {
|
||
return len(s) >= len(substr) && (s == substr ||
|
||
(len(s) > len(substr) && (findSubstring(s, substr) != -1)))
|
||
}
|
||
|
||
func findSubstring(s, substr string) int {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return i
|
||
}
|
||
}
|
||
return -1
|
||
}
|