package logic import ( "context" "encoding/json" "errors" "strings" "testing" "spark-server/internal/infrastructure/id" friendlogic "spark-server/internal/module/friend/logic" friendmodel "spark-server/internal/module/friend/model" "spark-server/internal/module/moment/entity" momentmodel "spark-server/internal/module/moment/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" "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 testMoment(t *testing.T) (*Service, *friendlogic.Service, *userlogic.Service) { t.Helper() ids, err := id.New(1) if err != nil { t.Fatal(err) } users := userlogic.NewService(usermodel.NewMemoryRepo(), usermodel.NewMemoryCodes(), ids, nil) friends := friendlogic.NewService(friendmodel.NewMemoryRepo(), users, ids, nil) svc := NewService(momentmodel.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 TestPublishFeedPrivacyAndDelete(t *testing.T) { svc, friends, users := testMoment(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") pub, err := svc.Publish(ctxA, a.ID, &PublishInput{Text: "visible"}) if err != nil || pub.Text != "visible" || pub.Kind != entity.KindText { t.Fatalf("%+v %v", pub, err) } feed, err := svc.Feed(ctxB, b.ID, 0, 20) if err != nil || len(feed.Items) != 1 || feed.Items[0].ID != pub.ID { t.Fatalf("feed %+v %v", feed, err) } priv, err := svc.Publish(ctxA, a.ID, &PublishInput{Text: "secret", Visibility: entity.VisibilityPrivate}) if err != nil { t.Fatal(err) } feed, err = svc.Feed(ctxB, b.ID, 0, 20) if err != nil || len(feed.Items) != 1 { t.Fatalf("private leaked %+v %v", feed, err) } if _, err := svc.Get(ctxB, b.ID, priv.ID); !errors.Is(err, errs.ErrNotFound) { t.Fatalf("private get %v", err) } album, err := svc.Album(ctxB, b.ID, a.ID, 0, 20) if err != nil || len(album.Items) != 1 { t.Fatalf("album %+v %v", album, err) } if err := svc.Delete(ctxA, a.ID, pub.ID); err != nil { t.Fatal(err) } feed, err = svc.Feed(ctxB, b.ID, 0, 20) if err != nil || len(feed.Items) != 0 { t.Fatalf("deleted %+v %v", feed, err) } } func TestLikeCommentNoticeAndHide(t *testing.T) { svc, friends, users := testMoment(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") post, err := svc.Publish(ctxA, a.ID, &PublishInput{Text: "hi"}) if err != nil { t.Fatal(err) } liked, err := svc.Like(ctxB, b.ID, post.ID) if err != nil || !liked.Liked || liked.LikeCount != 1 { t.Fatalf("like %+v %v", liked, err) } notices, err := svc.ListNotices(ctxA, a.ID, 0, 20) if err != nil || len(notices.Items) != 1 || notices.Items[0].Kind != entity.NoticeLike { t.Fatalf("notice %+v %v", notices, err) } commented, err := svc.Comment(ctxB, b.ID, post.ID, &CommentInput{Text: "nice"}) if err != nil || commented.CommentCount != 1 { t.Fatalf("comment %+v %v", commented, err) } unread, err := svc.UnreadFeed(ctxB, b.ID) if err != nil || !unread.HasNew || unread.LatestID != post.ID { t.Fatalf("unread %+v %v", unread, err) } if err := svc.MarkFeedRead(ctxB, b.ID, post.ID); err != nil { t.Fatal(err) } unread, err = svc.UnreadFeed(ctxB, b.ID) if err != nil || unread.HasNew { t.Fatalf("after read %+v %v", unread, err) } if _, err := svc.PutHide(ctxB, b.ID, a.ID, &HideInput{HideHim: true}); err != nil { t.Fatal(err) } feed, err := svc.Feed(ctxB, b.ID, 0, 20) if err != nil { t.Fatal(err) } for _, item := range feed.Items { if item.ID == post.ID { t.Fatal("hide him failed") } } } func TestMomentImageKeyRejectedIfChatPrefix(t *testing.T) { svc, _, users := testMoment(t) a, _ := twoAccounts(t, users) ctx := ctxUser(t, a.ID, "10.0.0.1") _, err := svc.Publish(ctx, a.ID, &PublishInput{ Images: []*entity.Image{{Key: "chat/image/20260918/xxxx.jpg"}}, }) if err == nil { t.Fatal("accepted chat key") } } func TestContentRoundTrip(t *testing.T) { raw, err := (&entity.Content{Images: []*entity.Image{{Key: "perm/moment/image/20260918/abcdefghijklmnop.jpg"}}}).Marshal() if err != nil { t.Fatal(err) } got, err := entity.UnmarshalContent(raw) if err != nil || got.Kind() != entity.KindImage { t.Fatalf("%s %v %v", raw, got, err) } if !json.Valid(raw) || strings.Contains(string(raw), "uid") { t.Fatalf("json %s", raw) } }