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) } }