spark-server/internal/module/group/logic/group_test.go
2026-09-18 14:50:41 +08:00

358 lines
12 KiB
Go

package logic
import (
"bytes"
"context"
"errors"
"image/jpeg"
"strconv"
"strings"
"testing"
"spark-server/internal/infrastructure/id"
convlogic "spark-server/internal/module/conversation/logic"
convmodel "spark-server/internal/module/conversation/model"
delivermodel "spark-server/internal/module/deliver/model"
friendlogic "spark-server/internal/module/friend/logic"
friendmodel "spark-server/internal/module/friend/model"
"spark-server/internal/module/group/entity"
groupmodel "spark-server/internal/module/group/model"
msgmodel "spark-server/internal/module/message/model"
userentity "spark-server/internal/module/user/entity"
userlogic "spark-server/internal/module/user/logic"
usermodel "spark-server/internal/module/user/model"
"spark-server/internal/pkg/errs"
)
func testGroups(t *testing.T) (*Service, *friendlogic.Service, *userlogic.Service) {
t.Helper()
return testGroupsStore(t, nil)
}
type testObjects interface {
Get(ctx context.Context, key string) ([]byte, error)
Put(ctx context.Context, key string, body []byte, contentType string) error
}
func testGroupsStore(t *testing.T, objects testObjects) (*Service, *friendlogic.Service, *userlogic.Service) {
t.Helper()
ids, err := id.New(1)
if err != nil {
t.Fatal(err)
}
var userObjs userlogic.Objects
var groupObjs Objects
if objects != nil {
userObjs = objects
groupObjs = objects
}
users := userlogic.NewService(usermodel.NewMemoryRepo(), usermodel.NewMemoryCodes(), ids, userObjs)
friends := friendlogic.NewService(
friendmodel.NewMemoryRepo(), users, ids,
friendlogic.NewGreeter(convlogic.NewP2PService(convmodel.NewMemoryP2P(), ids), msgmodel.NewMemoryRepo(), ids, delivermodel.NewMemoryQueue()),
)
groups := NewService(groupmodel.NewMemoryRepo(), users, friends, ids, msgmodel.NewMemoryRepo(), delivermodel.NewMemoryQueue(), groupObjs)
return groups, friends, users
}
func login(t *testing.T, users *userlogic.Service, email string, device uint64) *userentity.User {
t.Helper()
u, err := users.AuthenticateEmail(t.Context(), email, "1", &userentity.LoginDevice{DeviceID: device, DeviceType: userentity.DeviceIOS})
if err != nil {
t.Fatal(err)
}
return u
}
func becomeFriends(t *testing.T, friends *friendlogic.Service, a, b *userentity.User) {
t.Helper()
ctx := t.Context()
req, err := friends.Apply(ctx, a.ID, &friendlogic.ApplyInput{UID: b.ID})
if err != nil {
t.Fatal(err)
}
if _, err := friends.Accept(ctx, b.ID, friendlogic.AcceptInput{RequestID: req.RequestID}); err != nil {
t.Fatal(err)
}
}
func TestCreateGroupAutoIncrementID(t *testing.T) {
svc, friends, users := testGroups(t)
ctx := t.Context()
a := login(t, users, "test01@spark.com", 1)
b := login(t, users, "test02@spark.com", 2)
c := login(t, users, "test03@spark.com", 3)
becomeFriends(t, friends, a, b)
becomeFriends(t, friends, a, c)
g, err := svc.Create(ctx, a.ID, &CreateInput{Name: "周末局", MemberUIDs: []int64{b.ID, c.ID}})
if err != nil {
t.Fatal(err)
}
if g.ID != 1 || g.ConversationID == g.ID || g.ConversationID <= 0 || g.Avatar != "" {
t.Fatalf("id=%d conv=%d avatar=%q", g.ID, g.ConversationID, g.Avatar)
}
if g.MemberCount != 3 || g.Role != entity.RoleOwner || g.OwnerID != a.ID {
t.Fatalf("%+v", g)
}
second, err := svc.Create(ctx, a.ID, &CreateInput{Name: "第二群", MemberUIDs: []int64{b.ID}})
if err != nil || second.ID != 2 {
t.Fatalf("second id=%d err=%v", second.ID, err)
}
if err := svc.CanSend(ctx, b.ID, g.ConversationID); err != nil {
t.Fatal(err)
}
ids, err := svc.MemberIDs(ctx, g.ConversationID)
if err != nil || len(ids) != 3 {
t.Fatalf("members %v %v", ids, err)
}
gid, err := svc.IDByConversation(ctx, g.ConversationID)
if err != nil || gid != g.ID {
t.Fatalf("group of conv %d %v", gid, err)
}
}
func TestGroupInviteKickLeaveMute(t *testing.T) {
svc, friends, users := testGroups(t)
ctx := t.Context()
a := login(t, users, "test01@spark.com", 1)
b := login(t, users, "test02@spark.com", 2)
c := login(t, users, "test03@spark.com", 3)
becomeFriends(t, friends, a, b)
becomeFriends(t, friends, a, c)
becomeFriends(t, friends, b, c)
g, err := svc.Create(ctx, a.ID, &CreateInput{Name: "局", MemberUIDs: []int64{b.ID}})
if err != nil {
t.Fatal(err)
}
if _, err := svc.Create(ctx, a.ID, &CreateInput{Name: "x", MemberUIDs: []int64{999}}); err == nil {
t.Fatal("missing user")
}
if _, err := svc.Invite(ctx, a.ID, g.ID, &InviteInput{UIDs: []int64{c.ID}}); err != nil {
t.Fatal(err)
}
if err := svc.RemoveMember(ctx, a.ID, g.ID, a.ID); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("owner leave %v", err)
}
if err := svc.RemoveMember(ctx, a.ID, g.ID, c.ID); err != nil {
t.Fatal(err)
}
if err := svc.CanSend(ctx, c.ID, g.ConversationID); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("kicked send %v", err)
}
muted := true
if _, err := svc.UpdateMember(ctx, a.ID, g.ID, b.ID, &UpdateMemberInput{SpeakMuted: &muted}); err != nil {
t.Fatal(err)
}
if err := svc.CanSend(ctx, b.ID, g.ConversationID); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("muted send %v", err)
}
if err := svc.RemoveMember(ctx, b.ID, g.ID, b.ID); err != nil {
t.Fatal(err)
}
if err := svc.Dissolve(ctx, a.ID, g.ID); err != nil {
t.Fatal(err)
}
if _, err := svc.Get(ctx, a.ID, g.ID); !errors.Is(err, errs.ErrNotFound) {
t.Fatalf("dissolved %v", err)
}
}
func TestGroupAvatarKey(t *testing.T) {
svc, friends, users := testGroups(t)
ctx := t.Context()
a := login(t, users, "test01@spark.com", 1)
b := login(t, users, "test02@spark.com", 2)
becomeFriends(t, friends, a, b)
g, err := svc.Create(ctx, a.ID, &CreateInput{Name: "局", MemberUIDs: []int64{b.ID}})
if err != nil {
t.Fatal(err)
}
key := "perm/group/1/99.jpg"
if g.ID != 1 {
key = "perm/group/" + strconv.FormatInt(g.ID, 10) + "/99.jpg"
}
if _, err := svc.Update(ctx, a.ID, g.ID, &UpdateInput{Avatar: &key}); err != nil {
t.Fatal(err)
}
got, err := svc.Get(ctx, a.ID, g.ID)
if err != nil || got.Avatar != key {
t.Fatalf("%+v %v", got, err)
}
bad := "perm/avatar/1/x.jpg"
if _, err := svc.Update(ctx, a.ID, g.ID, &UpdateInput{Avatar: &bad}); err == nil {
t.Fatal("personal avatar key")
}
}
func TestCreateGroupCollageAvatar(t *testing.T) {
store := &memObjects{kv: map[string][]byte{}}
svc, friends, users := testGroupsStore(t, store)
ctx := t.Context()
a := login(t, users, "test01@spark.com", 1)
b := login(t, users, "test02@spark.com", 2)
becomeFriends(t, friends, a, b)
if a.Avatar == "" || b.Avatar == "" {
t.Fatal("register default avatar")
}
got, err := svc.Create(ctx, a.ID, &CreateInput{Name: "拼接头像", MemberUIDs: []int64{b.ID}})
if err != nil {
t.Fatal(err)
}
prefix := "perm/group/" + strconv.FormatInt(got.ID, 10) + "/"
if !strings.HasPrefix(got.Avatar, prefix) {
t.Fatalf("avatar %q", got.Avatar)
}
raw, ok := store.kv[got.Avatar]
if !ok || len(raw) == 0 {
t.Fatal("missing object")
}
img, err := jpeg.Decode(bytes.NewReader(raw))
if err != nil || img.Bounds().Dx() < 1 {
t.Fatalf("jpeg %+v %v", img, err)
}
}
func TestCreateGroupAvatarWhenMemberGetFails(t *testing.T) {
store := &noGetStore{kv: map[string][]byte{}}
svc, friends, users := testGroupsStore(t, store)
ctx := t.Context()
a := login(t, users, "test01@spark.com", 1)
b := login(t, users, "test02@spark.com", 2)
becomeFriends(t, friends, a, b)
got, err := svc.Create(ctx, a.ID, &CreateInput{Name: "默认头像", MemberUIDs: []int64{b.ID}})
if err != nil {
t.Fatal(err)
}
if got.Avatar == "" || !strings.HasPrefix(got.Avatar, "perm/group/") {
t.Fatalf("want default group avatar, got %q", got.Avatar)
}
if _, ok := store.kv[got.Avatar]; !ok {
t.Fatal("missing object")
}
}
type memObjects struct {
kv map[string][]byte
}
func (m *memObjects) Get(_ context.Context, key string) ([]byte, error) {
if m == nil || m.kv == nil {
return nil, errs.ErrNotFound
}
b, ok := m.kv[key]
if !ok {
return nil, errs.ErrNotFound
}
return append([]byte(nil), b...), nil
}
func (m *memObjects) Put(_ context.Context, key string, body []byte, _ string) error {
if m.kv == nil {
m.kv = map[string][]byte{}
}
m.kv[key] = append([]byte(nil), body...)
return nil
}
type noGetStore struct {
kv map[string][]byte
}
func (n *noGetStore) Get(_ context.Context, _ string) ([]byte, error) {
return nil, errs.ErrNotFound
}
func (n *noGetStore) Put(_ context.Context, key string, body []byte, _ string) error {
if n.kv == nil {
n.kv = map[string][]byte{}
}
n.kv[key] = append([]byte(nil), body...)
return nil
}
func TestGroupAdminAnnouncementRemarkMentions(t *testing.T) {
svc, friends, users := testGroups(t)
ctx := t.Context()
a := login(t, users, "test01@spark.com", 1)
b := login(t, users, "test02@spark.com", 2)
c := login(t, users, "test03@spark.com", 3)
becomeFriends(t, friends, a, b)
becomeFriends(t, friends, a, c)
becomeFriends(t, friends, b, c)
g, err := svc.Create(ctx, a.ID, &CreateInput{Name: "局", MemberUIDs: []int64{b.ID, c.ID}})
if err != nil {
t.Fatal(err)
}
name := "新名字"
announce := "今晚九点"
if _, err := svc.Update(ctx, b.ID, g.ID, &UpdateInput{Name: &name, Announcement: &announce}); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("member update %v", err)
}
if _, err := svc.Update(ctx, a.ID, g.ID, &UpdateInput{Name: &name, Announcement: &announce}); err != nil {
t.Fatal(err)
}
got, err := svc.Get(ctx, b.ID, g.ID)
if err != nil || got.Name != name || got.Announcement != announce {
t.Fatalf("profile %+v %v", got, err)
}
admin := entity.RoleAdmin
if _, err := svc.UpdateMember(ctx, a.ID, g.ID, b.ID, &UpdateMemberInput{Role: &admin}); err != nil {
t.Fatal(err)
}
muted := true
if _, err := svc.UpdateMember(ctx, b.ID, g.ID, c.ID, &UpdateMemberInput{SpeakMuted: &muted}); err != nil {
t.Fatal(err)
}
if _, err := svc.UpdateMember(ctx, b.ID, g.ID, a.ID, &UpdateMemberInput{SpeakMuted: &muted}); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("admin mute owner %v", err)
}
if err := svc.RemoveMember(ctx, b.ID, g.ID, c.ID); err != nil {
t.Fatal(err)
}
if _, err := svc.Invite(ctx, a.ID, g.ID, &InviteInput{UIDs: []int64{c.ID}}); err != nil {
t.Fatal(err)
}
if err := svc.CheckMentions(ctx, c.ID, g.ConversationID, true, nil); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("member at-all %v", err)
}
if _, err := svc.UpdateMember(ctx, a.ID, g.ID, c.ID, &UpdateMemberInput{Role: &admin}); err != nil {
t.Fatal(err)
}
if err := svc.RemoveMember(ctx, b.ID, g.ID, c.ID); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("admin kick admin %v", err)
}
if _, err := svc.UpdateMember(ctx, b.ID, g.ID, c.ID, &UpdateMemberInput{SpeakMuted: &muted}); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("admin mute admin %v", err)
}
nick := "群里的我"
remark := "周末球局"
if _, err := svc.UpdateMember(ctx, c.ID, g.ID, c.ID, &UpdateMemberInput{Nickname: &nick, Remark: &remark}); err != nil {
t.Fatal(err)
}
me, err := svc.Get(ctx, c.ID, g.ID)
if err != nil || me.GroupNickname != nick || me.Remark != remark {
t.Fatalf("self card %+v %v", me, err)
}
if _, err := svc.UpdateMember(ctx, b.ID, g.ID, c.ID, &UpdateMemberInput{Remark: &remark}); !errors.Is(err, errs.ErrForbidden) {
t.Fatalf("others remark %v", err)
}
if err := svc.CheckMentions(ctx, c.ID, g.ConversationID, false, []int64{a.ID}); err != nil {
t.Fatal(err)
}
if err := svc.CheckMentions(ctx, a.ID, g.ConversationID, true, nil); err != nil {
t.Fatal(err)
}
if err := svc.CheckMentions(ctx, b.ID, g.ConversationID, true, []int64{c.ID}); err != nil {
t.Fatal(err)
}
if err := svc.CheckMentions(ctx, a.ID, g.ConversationID, false, []int64{999}); !errors.Is(err, errs.ErrInvalidArgument) {
t.Fatalf("at outsider %v", err)
}
}