spark-server/internal/module/square/logic/square_test.go
DESKTOP-L8OUL9F\gfl0034 53426af122 feat: 多广场 channel、音乐公开 API 与订阅 package_name
支持 square/squarenovel/squaresteps/squarehealth 实例与 category 筛选;音乐增加免登录 grouped/track;订阅套餐按 package_name 区分;补 migration 0030/0031 与 OpenAPI。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 09:02:28 +08:00

292 lines
9.3 KiB
Go

package logic
import (
"context"
"errors"
"testing"
"spark-server/internal/infrastructure/id"
friendlogic "spark-server/internal/module/friend/logic"
friendmodel "spark-server/internal/module/friend/model"
"spark-server/internal/module/square/entity"
squaremodel "spark-server/internal/module/square/model"
userentity "spark-server/internal/module/user/entity"
userlogic "spark-server/internal/module/user/logic"
"spark-server/internal/module/user/logictest"
"spark-server/internal/pkg/errs"
"spark-server/internal/pkg/reqctx"
)
type memObj struct {
deleted []string
}
func (m *memObj) Delete(_ context.Context, key string) error {
m.deleted = append(m.deleted, key)
return nil
}
func testSquare(t *testing.T) (*Service, *friendlogic.Service, *userlogic.Service) {
t.Helper()
ids, err := id.New(1)
if err != nil {
t.Fatal(err)
}
users := logictest.Users(ids, nil)
friends := friendlogic.NewService(friendmodel.NewMemoryRepo(), users, ids, nil)
svc := NewService(squaremodel.NewMemoryRepo(), friends, users, &memObj{}, ids)
return svc, friends, users
}
func twoAccounts(t *testing.T, users *userlogic.Service) (*userentity.User, *userentity.User) {
t.Helper()
ctx := t.Context()
a, err := users.AuthenticateEmail(ctx, "test01@spark.com", "1", &userentity.LoginDevice{DeviceID: 1, DeviceType: userentity.DeviceIOS})
if err != nil {
t.Fatal(err)
}
b, err := users.AuthenticateEmail(ctx, "test02@spark.com", "1", &userentity.LoginDevice{DeviceID: 2, DeviceType: userentity.DeviceWeb})
if err != nil {
t.Fatal(err)
}
return a, b
}
func becomeFriends(t *testing.T, friends *friendlogic.Service, a, b *userentity.User) {
t.Helper()
ctx := t.Context()
applied, err := friends.Apply(ctx, a.ID, &friendlogic.ApplyInput{Email: "test02@spark.com"})
if err != nil {
t.Fatal(err)
}
if _, err := friends.Accept(ctx, b.ID, friendlogic.AcceptInput{RequestID: applied.RequestID}); err != nil {
t.Fatal(err)
}
}
func ctxUser(t *testing.T, uid int64, ip string) context.Context {
t.Helper()
return reqctx.With(t.Context(), &reqctx.RequestContext{UserID: uid, Language: "zh-CN", RealIP: ip})
}
func TestSquarePublishFeedPrivateAndNotice(t *testing.T) {
svc, friends, users := testSquare(t)
a, b := twoAccounts(t, users)
becomeFriends(t, friends, a, b)
ctxA := ctxUser(t, a.ID, "10.0.0.1")
ctxB := ctxUser(t, b.ID, "10.0.0.2")
ch := entity.ChannelSquare
pub, err := svc.Publish(ctxA, a.ID, ch, &PublishInput{Text: "hello square"})
if err != nil || pub.Text != "hello square" || pub.Channel != ch {
t.Fatalf("%+v %v", pub, err)
}
feed, err := svc.Feed(ctxB, b.ID, ch, FeedQuery{Limit: 20})
if err != nil || len(feed.Items) != 1 {
t.Fatalf("feed %+v %v", feed, err)
}
priv, err := svc.Publish(ctxA, a.ID, ch, &PublishInput{Text: "secret", Visibility: entity.VisibilityPrivate})
if err != nil {
t.Fatal(err)
}
feed, err = svc.Feed(ctxB, b.ID, ch, FeedQuery{Limit: 20})
if err != nil || len(feed.Items) != 1 {
t.Fatalf("private in feed %+v", feed)
}
if _, err := svc.Get(ctxB, b.ID, priv.ID, ch); !errors.Is(err, errs.ErrNotFound) {
t.Fatalf("private get %v", err)
}
liked, err := svc.Like(ctxB, b.ID, pub.ID, ch)
if err != nil || !liked.Liked {
t.Fatal(liked, err)
}
notices, err := svc.ListNotices(ctxA, a.ID, ch, 0, 20)
if err != nil || len(notices.Items) != 1 || notices.Items[0].Kind != entity.NoticeLike {
t.Fatalf("notice %+v %v", notices, err)
}
unread, err := svc.NoticeUnread(ctxA, a.ID, ch)
if err != nil || !unread.HasNew {
t.Fatalf("unread %+v", unread)
}
}
func TestSquareExcludeIncludeAndUserPosts(t *testing.T) {
svc, friends, users := testSquare(t)
a, b := twoAccounts(t, users)
becomeFriends(t, friends, a, b)
ctx := t.Context()
c, err := users.AuthenticateEmail(ctx, "test03@spark.com", "1", &userentity.LoginDevice{DeviceID: 3, DeviceType: userentity.DeviceIOS})
if err != nil {
t.Fatal(err)
}
ch := entity.ChannelSquare
ctxA := ctxUser(t, a.ID, "10.0.0.1")
ctxB := ctxUser(t, b.ID, "10.0.0.2")
ctxC := ctxUser(t, c.ID, "10.0.0.3")
excl, err := svc.Publish(ctxA, a.ID, ch, &PublishInput{
Text: "hide from b", Visibility: entity.VisibilityExclude, AudienceUIDs: []int64{b.ID},
})
if err != nil {
t.Fatal(err)
}
feedB, err := svc.Feed(ctxB, b.ID, ch, FeedQuery{Limit: 20})
if err != nil {
t.Fatal(err)
}
for _, item := range feedB.Items {
if item.ID == excl.ID {
t.Fatal("excluded user still sees post in feed")
}
}
feedC, err := svc.Feed(ctxC, c.ID, ch, FeedQuery{Limit: 20})
if err != nil || len(feedC.Items) != 1 {
t.Fatalf("stranger feed %+v %v", feedC, err)
}
incl, err := svc.Publish(ctxA, a.ID, ch, &PublishInput{
Text: "only b", Visibility: entity.VisibilityInclude, AudienceUIDs: []int64{b.ID},
})
if err != nil {
t.Fatal(err)
}
feedC, err = svc.Feed(ctxC, c.ID, ch, FeedQuery{Limit: 20})
if err != nil {
t.Fatal(err)
}
for _, item := range feedC.Items {
if item.ID == incl.ID {
t.Fatal("include post leaked to stranger feed")
}
}
album, err := svc.UserPosts(ctxB, b.ID, a.ID, ch, 0, 20)
if err != nil {
t.Fatal(err)
}
var sawIncl bool
for _, item := range album.Items {
if item.ID == excl.ID {
t.Fatal("excluded user sees post on profile")
}
if item.ID == incl.ID {
sawIncl = true
}
}
if !sawIncl {
t.Fatalf("include missing from album %+v", album)
}
if _, err := svc.Get(ctxB, b.ID, incl.ID, ch); err != nil {
t.Fatalf("include get %v", err)
}
}
func TestSquareUnlikeCommentDeleteAndPurge(t *testing.T) {
svc, friends, users := testSquare(t)
a, b := twoAccounts(t, users)
becomeFriends(t, friends, a, b)
ch := entity.ChannelSquare
ctxA := ctxUser(t, a.ID, "10.0.0.1")
ctxB := ctxUser(t, b.ID, "10.0.0.2")
post, err := svc.Publish(ctxA, a.ID, ch, &PublishInput{Text: "hi"})
if err != nil {
t.Fatal(err)
}
if _, err := svc.Like(ctxB, b.ID, post.ID, ch); err != nil {
t.Fatal(err)
}
unliked, err := svc.Unlike(ctxB, b.ID, post.ID, ch)
if err != nil || unliked.Liked || unliked.LikeCount != 0 {
t.Fatalf("unlike %+v %v", unliked, err)
}
commented, err := svc.Comment(ctxB, b.ID, post.ID, ch, &CommentInput{Text: "nice"})
if err != nil || commented.CommentCount != 1 {
t.Fatalf("comment %+v %v", commented, err)
}
cid := commented.Comments[0].ID
replied, err := svc.Comment(ctxA, a.ID, post.ID, ch, &CommentInput{Text: "thanks", ReplyToID: cid})
if err != nil || replied.CommentCount != 2 {
t.Fatalf("reply %+v %v", replied, err)
}
if _, err := svc.DeleteComment(ctxB, b.ID, post.ID, cid, ch); err != nil {
t.Fatal(err)
}
got, err := svc.Get(ctxA, a.ID, post.ID, ch)
if err != nil || got.CommentCount != 1 {
t.Fatalf("deleted comment %+v %v", got, err)
}
if err := svc.Delete(ctxA, a.ID, post.ID, ch); err != nil {
t.Fatal(err)
}
feed, err := svc.Feed(ctxB, b.ID, ch, FeedQuery{Limit: 20})
if err != nil || len(feed.Items) != 0 {
t.Fatalf("deleted feed %+v %v", feed, err)
}
post2, err := svc.Publish(ctxA, a.ID, ch, &PublishInput{
Images: []*entity.Image{{Key: "perm/square/image/20260922/abcdefghijklmnop.jpg"}},
})
if err != nil {
t.Fatal(err)
}
if err := svc.ReadNotices(ctxA, a.ID, ch, &NoticeReadInput{Cursor: 0}); err != nil {
t.Fatal(err)
}
unread, err := svc.NoticeUnread(ctxA, a.ID, ch)
if err != nil || unread.HasNew {
t.Fatalf("after read %+v %v", unread, err)
}
if err := svc.PurgeUser(ctxA, a.ID); err != nil {
t.Fatal(err)
}
if _, err := svc.Get(ctxA, a.ID, post2.ID, ch); !errors.Is(err, errs.ErrNotFound) {
t.Fatalf("purged get %v", err)
}
}
func TestSquareImageKeyRejectedIfChatPrefix(t *testing.T) {
svc, _, users := testSquare(t)
a, _ := twoAccounts(t, users)
ctx := ctxUser(t, a.ID, "10.0.0.1")
_, err := svc.Publish(ctx, a.ID, entity.ChannelSquare, &PublishInput{
Images: []*entity.Image{{Key: "chat/image/20260918/xxxx.jpg"}},
})
if err == nil {
t.Fatal("accepted chat key")
}
}
func TestSquareChannelIsolationAndCategory(t *testing.T) {
svc, friends, users := testSquare(t)
a, b := twoAccounts(t, users)
becomeFriends(t, friends, a, b)
ctxA := ctxUser(t, a.ID, "10.0.0.1")
ctxB := ctxUser(t, b.ID, "10.0.0.2")
main, err := svc.Publish(ctxA, a.ID, entity.ChannelSquare, &PublishInput{Text: "main", Category: entity.CategoryFunny})
if err != nil {
t.Fatal(err)
}
novel, err := svc.Publish(ctxA, a.ID, entity.ChannelSquareNovel, &PublishInput{Text: "novel", Category: entity.CategoryBeauty})
if err != nil {
t.Fatal(err)
}
feedMain, err := svc.Feed(ctxB, b.ID, entity.ChannelSquare, FeedQuery{Limit: 20})
if err != nil || len(feedMain.Items) != 1 || feedMain.Items[0].ID != main.ID {
t.Fatalf("main feed %+v %v", feedMain, err)
}
feedNovel, err := svc.Feed(ctxB, b.ID, entity.ChannelSquareNovel, FeedQuery{Limit: 20})
if err != nil || len(feedNovel.Items) != 1 || feedNovel.Items[0].ID != novel.ID {
t.Fatalf("novel feed %+v %v", feedNovel, err)
}
if _, err := svc.Get(ctxB, b.ID, novel.ID, entity.ChannelSquare); !errors.Is(err, errs.ErrNotFound) {
t.Fatalf("cross-channel get %v", err)
}
funny, err := svc.Feed(ctxB, b.ID, entity.ChannelSquare, FeedQuery{Category: entity.CategoryFunny, Limit: 20})
if err != nil || len(funny.Items) != 1 || funny.Items[0].Category != entity.CategoryFunny {
t.Fatalf("funny %+v %v", funny, err)
}
beauty, err := svc.Feed(ctxB, b.ID, entity.ChannelSquare, FeedQuery{Category: entity.CategoryBeauty, Limit: 20})
if err != nil || len(beauty.Items) != 0 {
t.Fatalf("beauty on main %+v %v", beauty, err)
}
cats := svc.Categories()
if len(cats) < 6 {
t.Fatalf("categories %+v", cats)
}
}