按 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 ./... 全部通过
732 lines
18 KiB
Go
732 lines
18 KiB
Go
package service
|
||
|
||
import (
|
||
"carrotskin/internal/model"
|
||
"context"
|
||
"testing"
|
||
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// TestProfileService_Validation 测试Profile服务验证逻辑
|
||
func TestProfileService_Validation(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
userID int64
|
||
profileName string
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "有效的用户ID和角色名",
|
||
userID: 1,
|
||
profileName: "TestProfile",
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "用户ID为0时无效",
|
||
userID: 0,
|
||
profileName: "TestProfile",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "角色名为空时无效",
|
||
userID: 1,
|
||
profileName: "",
|
||
wantValid: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
isValid := tt.userID > 0 && tt.profileName != ""
|
||
if isValid != tt.wantValid {
|
||
t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestProfileService_StatusValidation 测试用户状态验证
|
||
func TestProfileService_StatusValidation(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
status int16
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "状态为1(正常)时有效",
|
||
status: 1,
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "状态为0(禁用)时无效",
|
||
status: 0,
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "状态为-1(删除)时无效",
|
||
status: -1,
|
||
wantValid: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
isValid := tt.status == 1
|
||
if isValid != tt.wantValid {
|
||
t.Errorf("Status validation failed: got %v, want %v", isValid, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestUpdateProfile_PermissionCheck 测试更新Profile的权限检查逻辑
|
||
func TestUpdateProfile_PermissionCheck(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
profileUserID int64
|
||
requestUserID int64
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "用户ID匹配,允许操作",
|
||
profileUserID: 1,
|
||
requestUserID: 1,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "用户ID不匹配,拒绝操作",
|
||
profileUserID: 1,
|
||
requestUserID: 2,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hasError := tt.profileUserID != tt.requestUserID
|
||
if hasError != tt.wantErr {
|
||
t.Errorf("Permission check failed: got %v, want %v", hasError, tt.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestUpdateProfile_NameValidation 测试更新Profile时名称验证逻辑
|
||
func TestUpdateProfile_NameValidation(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
currentName string
|
||
newName *string
|
||
shouldCheck bool
|
||
}{
|
||
{
|
||
name: "名称未改变,不检查",
|
||
currentName: "TestProfile",
|
||
newName: stringPtr("TestProfile"),
|
||
shouldCheck: false,
|
||
},
|
||
{
|
||
name: "名称改变,需要检查",
|
||
currentName: "TestProfile",
|
||
newName: stringPtr("NewProfile"),
|
||
shouldCheck: true,
|
||
},
|
||
{
|
||
name: "名称为nil,不检查",
|
||
currentName: "TestProfile",
|
||
newName: nil,
|
||
shouldCheck: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
shouldCheck := tt.newName != nil && *tt.newName != tt.currentName
|
||
if shouldCheck != tt.shouldCheck {
|
||
t.Errorf("Name validation check failed: got %v, want %v", shouldCheck, tt.shouldCheck)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestDeleteProfile_PermissionCheck 测试删除Profile的权限检查
|
||
func TestDeleteProfile_PermissionCheck(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
profileUserID int64
|
||
requestUserID int64
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "用户ID匹配,允许删除",
|
||
profileUserID: 1,
|
||
requestUserID: 1,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "用户ID不匹配,拒绝删除",
|
||
profileUserID: 1,
|
||
requestUserID: 2,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hasError := tt.profileUserID != tt.requestUserID
|
||
if hasError != tt.wantErr {
|
||
t.Errorf("Permission check failed: got %v, want %v", hasError, tt.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestCheckProfileLimit_Logic 测试Profile数量限制检查逻辑
|
||
func TestCheckProfileLimit_Logic(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
count int
|
||
maxProfiles int
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "未达到上限",
|
||
count: 5,
|
||
maxProfiles: 10,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "达到上限",
|
||
count: 10,
|
||
maxProfiles: 10,
|
||
wantErr: true,
|
||
},
|
||
{
|
||
name: "超过上限",
|
||
count: 15,
|
||
maxProfiles: 10,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hasError := tt.count >= tt.maxProfiles
|
||
if hasError != tt.wantErr {
|
||
t.Errorf("Limit check failed: got %v, want %v", hasError, tt.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestValidateProfileByUserID_InputValidation 测试ValidateProfileByUserID输入验证
|
||
func TestValidateProfileByUserID_InputValidation(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
userID int64
|
||
uuid string
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "有效输入",
|
||
userID: 1,
|
||
uuid: "test-uuid",
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "userID为0",
|
||
userID: 0,
|
||
uuid: "test-uuid",
|
||
wantErr: true,
|
||
},
|
||
{
|
||
name: "uuid为空",
|
||
userID: 1,
|
||
uuid: "",
|
||
wantErr: true,
|
||
},
|
||
{
|
||
name: "两者都无效",
|
||
userID: 0,
|
||
uuid: "",
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
hasError := tt.userID == 0 || tt.uuid == ""
|
||
if hasError != tt.wantErr {
|
||
t.Errorf("Input validation failed: got %v, want %v", hasError, tt.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestValidateProfileByUserID_UserIDMatching 测试用户ID匹配逻辑
|
||
func TestValidateProfileByUserID_UserIDMatching(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
profileUserID int64
|
||
requestUserID int64
|
||
wantValid bool
|
||
}{
|
||
{
|
||
name: "用户ID匹配",
|
||
profileUserID: 1,
|
||
requestUserID: 1,
|
||
wantValid: true,
|
||
},
|
||
{
|
||
name: "用户ID不匹配",
|
||
profileUserID: 1,
|
||
requestUserID: 2,
|
||
wantValid: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
isValid := tt.profileUserID == tt.requestUserID
|
||
if isValid != tt.wantValid {
|
||
t.Errorf("UserID matching failed: got %v, want %v", isValid, tt.wantValid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGenerateRSAPrivateKey 测试RSA私钥生成
|
||
func TestGenerateRSAPrivateKey(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
wantError bool
|
||
}{
|
||
{
|
||
name: "生成RSA私钥",
|
||
wantError: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
privateKey, err := generateRSAPrivateKeyInternal()
|
||
if (err != nil) != tt.wantError {
|
||
t.Errorf("generateRSAPrivateKeyInternal() error = %v, wantError %v", err, tt.wantError)
|
||
return
|
||
}
|
||
if !tt.wantError {
|
||
if privateKey == "" {
|
||
t.Error("generateRSAPrivateKeyInternal() 返回的私钥不应为空")
|
||
}
|
||
// 验证PEM格式
|
||
if len(privateKey) < 100 {
|
||
t.Errorf("generateRSAPrivateKeyInternal() 返回的私钥长度异常: %d", len(privateKey))
|
||
}
|
||
// 验证包含PEM头部
|
||
if !contains(privateKey, "BEGIN RSA PRIVATE KEY") {
|
||
t.Error("generateRSAPrivateKeyInternal() 返回的私钥应包含PEM头部")
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGenerateRSAPrivateKey_Uniqueness 测试RSA私钥唯一性
|
||
func TestGenerateRSAPrivateKey_Uniqueness(t *testing.T) {
|
||
keys := make(map[string]bool)
|
||
for i := 0; i < 10; i++ {
|
||
key, err := generateRSAPrivateKeyInternal()
|
||
if err != nil {
|
||
t.Fatalf("generateRSAPrivateKeyInternal() 失败: %v", err)
|
||
}
|
||
if keys[key] {
|
||
t.Errorf("第%d次生成的密钥与之前重复", i+1)
|
||
}
|
||
keys[key] = true
|
||
}
|
||
}
|
||
|
||
// 辅助函数
|
||
func stringPtr(s string) *string {
|
||
return &s
|
||
}
|
||
|
||
func contains(s, substr string) bool {
|
||
return len(s) >= len(substr) && (s == substr ||
|
||
(len(s) > len(substr) && (s[:len(substr)] == substr ||
|
||
s[len(s)-len(substr):] == substr ||
|
||
containsMiddle(s, substr))))
|
||
}
|
||
|
||
func containsMiddle(s, substr string) bool {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ============================================================================
|
||
// 使用 Mock 的集成测试
|
||
// ============================================================================
|
||
|
||
// TestProfileServiceImpl_Create 测试创建Profile
|
||
func TestProfileServiceImpl_Create(t *testing.T) {
|
||
profileRepo := NewMockProfileRepository()
|
||
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()
|
||
profileService := NewProfileService(profileRepo, userRepo, cacheManager, logger)
|
||
|
||
tests := []struct {
|
||
name string
|
||
userID int64
|
||
profileName string
|
||
wantErr bool
|
||
errMsg string
|
||
setupMocks func()
|
||
}{
|
||
{
|
||
name: "正常创建Profile",
|
||
userID: 1,
|
||
profileName: "TestProfile",
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "用户不存在",
|
||
userID: 999,
|
||
profileName: "TestProfile2",
|
||
wantErr: true,
|
||
errMsg: "用户不存在",
|
||
},
|
||
{
|
||
name: "角色名已存在",
|
||
userID: 1,
|
||
profileName: "ExistingProfile",
|
||
wantErr: true,
|
||
errMsg: "角色名已被使用",
|
||
setupMocks: func() {
|
||
_ = profileRepo.Create(context.Background(), &model.Profile{
|
||
UUID: "existing-uuid",
|
||
UserID: 2,
|
||
Name: "ExistingProfile",
|
||
})
|
||
},
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if tt.setupMocks != nil {
|
||
tt.setupMocks()
|
||
}
|
||
|
||
ctx := context.Background()
|
||
profile, err := profileService.Create(ctx, tt.userID, tt.profileName)
|
||
|
||
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 err != nil {
|
||
t.Errorf("不期望返回错误: %v", err)
|
||
return
|
||
}
|
||
if profile == nil {
|
||
t.Error("返回的Profile不应为nil")
|
||
}
|
||
if profile.Name != tt.profileName {
|
||
t.Errorf("Profile名称不匹配: got %v, want %v", profile.Name, tt.profileName)
|
||
}
|
||
if profile.UUID == "" {
|
||
t.Error("Profile UUID不应为空")
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestProfileServiceImpl_GetByUUID 测试获取Profile
|
||
func TestProfileServiceImpl_GetByUUID(t *testing.T) {
|
||
profileRepo := NewMockProfileRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 预置Profile
|
||
testProfile := &model.Profile{
|
||
UUID: "test-uuid-123",
|
||
UserID: 1,
|
||
Name: "TestProfile",
|
||
}
|
||
_ = profileRepo.Create(context.Background(), testProfile)
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
profileService := NewProfileService(profileRepo, userRepo, cacheManager, logger)
|
||
|
||
tests := []struct {
|
||
name string
|
||
uuid string
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "获取存在的Profile",
|
||
uuid: "test-uuid-123",
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "获取不存在的Profile",
|
||
uuid: "non-existent-uuid",
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx := context.Background()
|
||
profile, err := profileService.GetByUUID(ctx, tt.uuid)
|
||
|
||
if tt.wantErr {
|
||
if err == nil {
|
||
t.Error("期望返回错误,但实际没有错误")
|
||
}
|
||
} else {
|
||
if err != nil {
|
||
t.Errorf("不期望返回错误: %v", err)
|
||
return
|
||
}
|
||
if profile == nil {
|
||
t.Error("返回的Profile不应为nil")
|
||
}
|
||
if profile.UUID != tt.uuid {
|
||
t.Errorf("Profile UUID不匹配: got %v, want %v", profile.UUID, tt.uuid)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestProfileServiceImpl_Delete 测试删除Profile
|
||
func TestProfileServiceImpl_Delete(t *testing.T) {
|
||
profileRepo := NewMockProfileRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 预置Profile
|
||
testProfile := &model.Profile{
|
||
UUID: "delete-test-uuid",
|
||
UserID: 1,
|
||
Name: "DeleteTestProfile",
|
||
}
|
||
_ = profileRepo.Create(context.Background(), testProfile)
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
profileService := NewProfileService(profileRepo, userRepo, cacheManager, logger)
|
||
|
||
tests := []struct {
|
||
name string
|
||
uuid string
|
||
userID int64
|
||
wantErr bool
|
||
}{
|
||
{
|
||
name: "正常删除",
|
||
uuid: "delete-test-uuid",
|
||
userID: 1,
|
||
wantErr: false,
|
||
},
|
||
{
|
||
name: "用户ID不匹配",
|
||
uuid: "delete-test-uuid",
|
||
userID: 2,
|
||
wantErr: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx := context.Background()
|
||
err := profileService.Delete(ctx, tt.uuid, tt.userID)
|
||
|
||
if tt.wantErr {
|
||
if err == nil {
|
||
t.Error("期望返回错误,但实际没有错误")
|
||
}
|
||
} else {
|
||
if err != nil {
|
||
t.Errorf("不期望返回错误: %v", err)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestProfileServiceImpl_GetByUserID 测试按用户获取档案列表
|
||
func TestProfileServiceImpl_GetByUserID(t *testing.T) {
|
||
profileRepo := NewMockProfileRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 为用户 1 和 2 预置不同档案
|
||
_ = profileRepo.Create(context.Background(), &model.Profile{UUID: "p1", UserID: 1, Name: "P1"})
|
||
_ = profileRepo.Create(context.Background(), &model.Profile{UUID: "p2", UserID: 1, Name: "P2"})
|
||
_ = profileRepo.Create(context.Background(), &model.Profile{UUID: "p3", UserID: 2, Name: "P3"})
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
svc := NewProfileService(profileRepo, userRepo, cacheManager, logger)
|
||
|
||
ctx := context.Background()
|
||
list, err := svc.GetByUserID(ctx, 1)
|
||
if err != nil {
|
||
t.Fatalf("GetByUserID 失败: %v", err)
|
||
}
|
||
if len(list) != 2 {
|
||
t.Fatalf("GetByUserID 返回数量错误, got=%d, want=2", len(list))
|
||
}
|
||
}
|
||
|
||
// TestProfileServiceImpl_Update 测试 Update
|
||
func TestProfileServiceImpl_Update(t *testing.T) {
|
||
profileRepo := NewMockProfileRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
profile := &model.Profile{
|
||
UUID: "u1",
|
||
UserID: 1,
|
||
Name: "OldName",
|
||
}
|
||
_ = profileRepo.Create(context.Background(), profile)
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
svc := NewProfileService(profileRepo, userRepo, cacheManager, logger)
|
||
|
||
ctx := context.Background()
|
||
|
||
// 正常更新名称与皮肤/披风
|
||
newName := "NewName"
|
||
var skinID int64 = 10
|
||
var capeID int64 = 20
|
||
updated, err := svc.Update(ctx, "u1", 1, &newName, &skinID, &capeID)
|
||
if err != nil {
|
||
t.Fatalf("Update 正常情况失败: %v", err)
|
||
}
|
||
if updated == nil || updated.Name != newName {
|
||
t.Fatalf("Update 未更新名称, got=%+v", updated)
|
||
}
|
||
|
||
// 用户无权限
|
||
if _, err := svc.Update(ctx, "u1", 2, &newName, nil, nil); err == nil {
|
||
t.Fatalf("Update 在无权限时应返回错误")
|
||
}
|
||
|
||
// 名称重复
|
||
_ = profileRepo.Create(context.Background(), &model.Profile{
|
||
UUID: "u2",
|
||
UserID: 2,
|
||
Name: "Duplicate",
|
||
})
|
||
if _, err := svc.Update(ctx, "u1", 1, stringPtr("Duplicate"), nil, nil); err == nil {
|
||
t.Fatalf("Update 在名称重复时应返回错误")
|
||
}
|
||
}
|
||
|
||
// TestProfileServiceImpl_CheckLimit_And_GetByNames 测试 CheckLimit / GetByNames / GetByProfileName
|
||
func TestProfileServiceImpl_CheckLimit_And_GetByNames(t *testing.T) {
|
||
profileRepo := NewMockProfileRepository()
|
||
userRepo := NewMockUserRepository()
|
||
logger := zap.NewNop()
|
||
|
||
// 为用户 1 预置 2 个档案
|
||
_ = profileRepo.Create(context.Background(), &model.Profile{UUID: "a", UserID: 1, Name: "A"})
|
||
_ = profileRepo.Create(context.Background(), &model.Profile{UUID: "b", UserID: 1, Name: "B"})
|
||
|
||
cacheManager := NewMockCacheManager()
|
||
svc := NewProfileService(profileRepo, userRepo, cacheManager, logger)
|
||
|
||
ctx := context.Background()
|
||
|
||
// CheckLimit 未达上限
|
||
if err := svc.CheckLimit(ctx, 1, 3); err != nil {
|
||
t.Fatalf("CheckLimit 未达到上限时不应报错: %v", err)
|
||
}
|
||
|
||
// CheckLimit 达到上限
|
||
if err := svc.CheckLimit(ctx, 1, 2); err == nil {
|
||
t.Fatalf("CheckLimit 达到上限时应报错")
|
||
}
|
||
|
||
// GetByNames
|
||
list, err := svc.GetByNames(ctx, []string{"A", "B"})
|
||
if err != nil {
|
||
t.Fatalf("GetByNames 失败: %v", err)
|
||
}
|
||
if len(list) != 2 {
|
||
t.Fatalf("GetByNames 返回数量错误, got=%d, want=2", len(list))
|
||
}
|
||
|
||
// GetByProfileName 存在
|
||
p, err := svc.GetByProfileName(ctx, "A")
|
||
if err != nil || p == nil || p.Name != "A" {
|
||
t.Fatalf("GetByProfileName 返回错误, profile=%+v, err=%v", p, err)
|
||
}
|
||
|
||
// ProfileSearchByName:小写名查询、去重、过滤空名、超限拒单
|
||
got, err := svc.ProfileSearchByName(ctx, []string{"a", "A", "b", "", " "}, 10)
|
||
if err != nil {
|
||
t.Fatalf("ProfileSearchByName 失败: %v", err)
|
||
}
|
||
if len(got) != 2 {
|
||
t.Fatalf("ProfileSearchByName 应去重返回 2 项, got=%d: %+v", len(got), got)
|
||
}
|
||
byName := make(map[string]string, len(got))
|
||
for _, n := range got {
|
||
byName[n.Name] = n.ID
|
||
}
|
||
if byName["A"] != "a" || byName["B"] != "b" {
|
||
t.Fatalf("返回的 id/name 映射错误: %+v", byName)
|
||
}
|
||
|
||
// 超过 maxBatch 应报错
|
||
if _, err := svc.ProfileSearchByName(ctx, []string{"a", "b", "c"}, 2); err == nil {
|
||
t.Fatalf("ProfileSearchByName 超过 maxBatch 应返回错误")
|
||
}
|
||
|
||
// ProfileSearchByNameSingle:存在 / 不存在 / 空名
|
||
single, err := svc.ProfileSearchByNameSingle(ctx, "a")
|
||
if err != nil || single == nil || single.ID != "a" || single.Name != "A" {
|
||
t.Fatalf("ProfileSearchByNameSingle 存在用例失败, got=%+v err=%v", single, err)
|
||
}
|
||
// 小写名应能命中大小写不敏感查询
|
||
singleLower, err := svc.ProfileSearchByNameSingle(ctx, "a")
|
||
if err != nil || singleLower == nil {
|
||
t.Fatalf("ProfileSearchByNameSingle 小写名匹配失败, got=%+v err=%v", singleLower, err)
|
||
}
|
||
// 不存在
|
||
missing, err := svc.ProfileSearchByNameSingle(ctx, "nope")
|
||
if err != nil || missing != nil {
|
||
t.Fatalf("ProfileSearchByNameSingle 不存在应返回 (nil,nil), got=%+v err=%v", missing, err)
|
||
}
|
||
// 空名
|
||
emptyName, err := svc.ProfileSearchByNameSingle(ctx, " ")
|
||
if err != nil || emptyName != nil {
|
||
t.Fatalf("ProfileSearchByNameSingle 空名应返回 (nil,nil), got=%+v err=%v", emptyName, err)
|
||
}
|
||
}
|