package service import ( "context" "testing" "carrotskin/internal/model" "carrotskin/internal/repository" "carrotskin/pkg/utils" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // newFriendsTestDB 构建内存 sqlite 并迁移所需表 func newFriendsTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.Profile{}, &model.Friend{}, &model.PlayerAttributes{}); err != nil { t.Fatalf("migrate: %v", err) } // 清理共享内存库的残留数据 db.Exec("DELETE FROM friends") db.Exec("DELETE FROM profiles") db.Exec("DELETE FROM player_attributes") return db } // newFriendsService 用真实 GORM repo + nil redis 构造服务(presence/blocklist 缓存路径会跳过) func newFriendsService(t *testing.T) (FriendsService, *gorm.DB) { t.Helper() db := newFriendsTestDB(t) friendRepo := repository.NewFriendRepository(db) attrRepo := repository.NewPlayerAttributeRepository(db) profileRepo := repository.NewProfileRepository(db) return NewFriendsService(friendRepo, attrRepo, profileRepo, nil, zap.NewNop()), db } func mustCreateProfile(t *testing.T, db *gorm.DB, name string) *model.Profile { t.Helper() p := &model.Profile{ UUID: utils.GenerateUUID(), UserID: 1, Name: name, RSAPrivateKey: "x", } if err := db.Create(p).Error; err != nil { t.Fatalf("create profile %s: %v", name, err) } return p } // TestAddFriend_SelfReject 添加自己应报错 func TestAddFriend_SelfReject(t *testing.T) { svc, db := newFriendsService(t) p := mustCreateProfile(t, db, "self") _, err := svc.PerformFriendAction(context.Background(), p.UUID, FriendActionRequest{ ProfileID: p.UUID, UpdateType: FriendActionAdd, }) if err == nil { t.Fatal("添加自己应报错") } } // TestAddFriend_UnknownProfile 目标不存在应报错 func TestAddFriend_UnknownProfile(t *testing.T) { svc, db := newFriendsService(t) p := mustCreateProfile(t, db, "a") // 使用 32 位无连字符 UUID,规避 FormatUUIDToNoDash 对非法长度的告警路径 _, err := svc.PerformFriendAction(context.Background(), p.UUID, FriendActionRequest{ ProfileID: "ffffffffffffffffffffffffffffffff", UpdateType: FriendActionAdd, }) if err == nil { t.Fatal("不存在的目标应报错") } } // TestAddFriend_SendRequestThenAccept 双向流程:A 发请求 -> B 接受 -> 互为好友 func TestAddFriend_SendRequestThenAccept(t *testing.T) { svc, db := newFriendsService(t) a := mustCreateProfile(t, db, "alice") b := mustCreateProfile(t, db, "bob") // A 向 B 发起请求 resp, err := svc.PerformFriendAction(context.Background(), a.UUID, FriendActionRequest{ ProfileID: b.UUID, UpdateType: FriendActionAdd, }) if err != nil { t.Fatalf("A 发起请求失败: %v", err) } if len(resp.OutgoingRequests) != 1 || resp.OutgoingRequests[0].ProfileID != b.UUID { t.Fatalf("A 的 outgoing 应包含 B, got %+v", resp.OutgoingRequests) } // B 视角:incoming 应包含 A respB, err := svc.ListFriends(context.Background(), b.UUID) if err != nil { t.Fatalf("B 查询列表失败: %v", err) } if len(respB.IncomingRequests) != 1 || respB.IncomingRequests[0].ProfileID != a.UUID { t.Fatalf("B 的 incoming 应包含 A, got %+v", respB.IncomingRequests) } // B 接受请求(B 用 ADD 指向 A -> 自动接受) resp, err = svc.PerformFriendAction(context.Background(), b.UUID, FriendActionRequest{ ProfileID: a.UUID, UpdateType: FriendActionAdd, }) if err != nil { t.Fatalf("B 接受请求失败: %v", err) } if len(resp.Friends) != 1 || resp.Friends[0].ProfileID != a.UUID { t.Fatalf("B 的 friends 应包含 A, got %+v", resp.Friends) } // A 视角:也是好友 respA, err := svc.ListFriends(context.Background(), a.UUID) if err != nil { t.Fatalf("A 查询列表失败: %v", err) } if len(respA.Friends) != 1 || respA.Friends[0].ProfileID != b.UUID { t.Fatalf("A 的 friends 应包含 B, got %+v", respA.Friends) } } // TestRemoveFriend_Flow A B 互为好友后 A 删除好友 -> 双方都无好友 func TestRemoveFriend_Flow(t *testing.T) { svc, db := newFriendsService(t) a := mustCreateProfile(t, db, "alice") b := mustCreateProfile(t, db, "bob") // 互为好友 if _, err := svc.PerformFriendAction(context.Background(), a.UUID, FriendActionRequest{ProfileID: b.UUID, UpdateType: FriendActionAdd}); err != nil { t.Fatal(err) } if _, err := svc.PerformFriendAction(context.Background(), b.UUID, FriendActionRequest{ProfileID: a.UUID, UpdateType: FriendActionAdd}); err != nil { t.Fatal(err) } // A 删除好友 if _, err := svc.PerformFriendAction(context.Background(), a.UUID, FriendActionRequest{ProfileID: b.UUID, UpdateType: FriendActionRemove}); err != nil { t.Fatalf("A 删除好友失败: %v", err) } // B 视角应不再有好友 respB, err := svc.ListFriends(context.Background(), b.UUID) if err != nil { t.Fatal(err) } if len(respB.Friends) != 0 { t.Fatalf("B 应无好友, got %+v", respB.Friends) } } // TestDeclineIncomingRequest B 拒绝 A 的请求 -> A 的 outgoing 清空 func TestDeclineIncomingRequest(t *testing.T) { svc, db := newFriendsService(t) a := mustCreateProfile(t, db, "alice") b := mustCreateProfile(t, db, "bob") if _, err := svc.PerformFriendAction(context.Background(), a.UUID, FriendActionRequest{ProfileID: b.UUID, UpdateType: FriendActionAdd}); err != nil { t.Fatal(err) } // B 拒绝(B 用 REMOVE 指向 A) if _, err := svc.PerformFriendAction(context.Background(), b.UUID, FriendActionRequest{ProfileID: a.UUID, UpdateType: FriendActionRemove}); err != nil { t.Fatalf("B 拒绝失败: %v", err) } // A 的 outgoing 应清空 respA, err := svc.ListFriends(context.Background(), a.UUID) if err != nil { t.Fatal(err) } if len(respA.OutgoingRequests) != 0 { t.Fatalf("A 的 outgoing 应已清空, got %+v", respA.OutgoingRequests) } } // TestGetAttributes_Default 默认属性 func TestGetAttributes_Default(t *testing.T) { svc, db := newFriendsService(t) p := mustCreateProfile(t, db, "a") resp, err := svc.GetAttributes(context.Background(), p.UUID) if err != nil { t.Fatal(err) } if !resp.ProfanityFilterPreferences.ProfanityFilterOn { t.Error("默认应启用脏词过滤") } if resp.FriendsPreferences.Friends != "ENABLED" { t.Errorf("默认 friends = %q, want ENABLED", resp.FriendsPreferences.Friends) } } // TestUpdateAttributes_PartialUpdate 部分更新保留其它字段 func TestUpdateAttributes_PartialUpdate(t *testing.T) { svc, db := newFriendsService(t) p := mustCreateProfile(t, db, "a") off := false if err := svc.UpdateAttributes(context.Background(), p.UUID, PlayerAttributesRequest{ ProfanityFilterPreferences: &ProfanityFilterPreferences{ProfanityFilterOn: &off}, }); err != nil { t.Fatalf("UpdateAttributes err: %v", err) } var row model.PlayerAttributes if err := db.First(&row, "profile_uuid = ?", p.UUID).Error; err != nil { t.Fatalf("查询 row 失败: %v", err) } t.Logf("DB row: %+v", row) resp, err := svc.GetAttributes(context.Background(), p.UUID) if err != nil { t.Fatalf("GetAttributes err: %v", err) } if resp.ProfanityFilterPreferences.ProfanityFilterOn { t.Error("脏词过滤应已关闭") } if resp.FriendsPreferences.Friends != "ENABLED" { t.Errorf("friends 应保留 ENABLED, got %q", resp.FriendsPreferences.Friends) } } // TestBlocklist_BlockUnblock 屏蔽/取消屏蔽 func TestBlocklist_BlockUnblock(t *testing.T) { svc, db := newFriendsService(t) a := mustCreateProfile(t, db, "alice") b := mustCreateProfile(t, db, "bob") // 使用类型断言访问 Block/Unblock(接口未暴露,但实现有) fs := svc.(*friendsService) if err := fs.Block(context.Background(), a.UUID, b.UUID); err != nil { t.Fatalf("屏蔽失败: %v", err) } resp, err := svc.GetBlocklist(context.Background(), a.UUID) if err != nil { t.Fatal(err) } if len(resp.BlockedProfiles) != 1 || resp.BlockedProfiles[0] != b.UUID { t.Fatalf("屏蔽列表应含 B, got %+v", resp.BlockedProfiles) } if err := fs.Unblock(context.Background(), a.UUID, b.UUID); err != nil { t.Fatalf("取消屏蔽失败: %v", err) } resp, err = svc.GetBlocklist(context.Background(), a.UUID) if err != nil { t.Fatal(err) } if len(resp.BlockedProfiles) != 0 { t.Fatalf("屏蔽列表应空, got %+v", resp.BlockedProfiles) } }