434 lines
14 KiB
Go
434 lines
14 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)
|
|
}
|
|
}
|
|
|
|
func TestPurgeUserTransfersOwnerToAdmin(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{c.ID, b.ID}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
admin := entity.RoleAdmin
|
|
if _, err := svc.UpdateMember(ctx, a.ID, g.ID, b.ID, &UpdateMemberInput{Role: &admin}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.PurgeUser(ctx, a.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := svc.Get(ctx, b.ID, g.ID)
|
|
if err != nil || got.OwnerID != b.ID || got.Role != entity.RoleOwner {
|
|
t.Fatalf("successor %+v %v", got, err)
|
|
}
|
|
if _, err := svc.Get(ctx, a.ID, g.ID); !errors.Is(err, errs.ErrForbidden) {
|
|
t.Fatalf("owner still member %v", err)
|
|
}
|
|
if _, err := svc.Get(ctx, c.ID, g.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestPurgeUserTransfersOwnerToFirstMember(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 err := svc.PurgeUser(ctx, a.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := svc.Get(ctx, b.ID, g.ID)
|
|
if err != nil || got.OwnerID != b.ID {
|
|
t.Fatalf("first member %+v %v", got, err)
|
|
}
|
|
if _, err := svc.Get(ctx, a.ID, g.ID); !errors.Is(err, errs.ErrForbidden) {
|
|
t.Fatalf("owner still member %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPurgeUserDissolvesWhenOwnerAlone(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)
|
|
}
|
|
if err := svc.RemoveMember(ctx, a.ID, g.ID, b.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.PurgeUser(ctx, a.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.Get(ctx, a.ID, g.ID); !errors.Is(err, errs.ErrNotFound) {
|
|
t.Fatalf("solo owner group %v", err)
|
|
}
|
|
}
|