261 lines
8.4 KiB
Go
261 lines
8.4 KiB
Go
|
|
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)
|
|||
|
|
}
|
|||
|
|
}
|