package logic import ( "context" "errors" "strings" "testing" "time" "spark-server/internal/infrastructure/id" convlogic "spark-server/internal/module/conversation/logic" convmodel "spark-server/internal/module/conversation/model" favmodel "spark-server/internal/module/favorite/model" msgentity "spark-server/internal/module/message/entity" msgmodel "spark-server/internal/module/message/model" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/reqctx" ) type memObjects struct { keys map[string]bool } func (m *memObjects) Copy(_ context.Context, src, dst string) error { if m.keys == nil || !m.keys[src] { return errs.ErrMediaExpired } m.keys[dst] = true return nil } func (m *memObjects) Delete(_ context.Context, key string) error { delete(m.keys, key) return nil } func testFav(t *testing.T) (*Service, *msgmodel.MemoryRepo, *convmodel.MemoryP2P, *memObjects, *id.Generator) { t.Helper() ids, err := id.New(1) if err != nil { t.Fatal(err) } msgs := msgmodel.NewMemoryRepo() convs := convmodel.NewMemoryP2P() objs := &memObjects{keys: map[string]bool{}} svc := NewService(favmodel.NewMemoryRepo(), msgs, convs, objs, ids) return svc, msgs, convs, objs, ids } func TestFavoriteCopiesMediaAndExpires(t *testing.T) { svc, msgs, convs, objs, ids := testFav(t) ctx := reqctx.With(t.Context(), &reqctx.RequestContext{UserID: 1, Language: "zh-CN"}) p2p := convlogic.NewP2PService(convs, ids) convID, err := p2p.EnsureP2P(ctx, 1, 2) if err != nil { t.Fatal(err) } src := "chat/20260917/1/99.jpg" thumb := "chat/20260917/1/t.jpg" orig := "chat/20260917/1/o.jpg" objs.keys[src] = true objs.keys[thumb] = true objs.keys[orig] = true raw, err := msgentity.NewImage(&msgentity.Image{Key: src, ThumbKey: thumb, OrigKey: orig, Mime: "image/jpeg"}).Marshal() if err != nil { t.Fatal(err) } msgID, err := ids.NextID() if err != nil { t.Fatal(err) } if err := msgs.Create(ctx, &msgentity.Message{ ID: msgID, ConversationID: convID, SenderID: 1, ClientMsgID: "c1", Type: msgentity.TypeImage, Content: raw, Status: msgentity.StatusNormal, CreatedAtMS: time.Now().UnixMilli(), }); err != nil { t.Fatal(err) } item, err := svc.Create(ctx, 1, &CreateInput{MsgID: msgID}) if err != nil { t.Fatal(err) } if item.MsgID != msgID || !strings.Contains(string(item.Content), "perm/fav/") { t.Fatalf("%+v", item) } if strings.Contains(string(item.Content), src) || strings.Contains(string(item.Content), orig) { t.Fatal("still points at chat key") } again, err := svc.Create(ctx, 1, &CreateInput{MsgID: msgID}) if err != nil || again.ID != item.ID { t.Fatalf("idempotent %+v %v", again, err) } expiredID, err := ids.NextID() if err != nil { t.Fatal(err) } gone := "chat/20260917/1/gone.jpg" raw2, err := msgentity.NewImage(&msgentity.Image{Key: gone, Mime: "image/jpeg"}).Marshal() if err != nil { t.Fatal(err) } if err := msgs.Create(ctx, &msgentity.Message{ ID: expiredID, ConversationID: convID, SenderID: 1, ClientMsgID: "c2", Type: msgentity.TypeImage, Content: raw2, Status: msgentity.StatusNormal, }); err != nil { t.Fatal(err) } _, err = svc.Create(ctx, 1, &CreateInput{MsgID: expiredID}) if !errors.Is(err, errs.ErrMediaExpired) { t.Fatalf("expired %v", err) } } func TestFavoriteForbidsNonMember(t *testing.T) { svc, msgs, convs, _, ids := testFav(t) ctx := t.Context() p2p := convlogic.NewP2PService(convs, ids) convID, err := p2p.EnsureP2P(ctx, 1, 2) if err != nil { t.Fatal(err) } raw, err := msgentity.NewText("hi").Marshal() if err != nil { t.Fatal(err) } msgID, err := ids.NextID() if err != nil { t.Fatal(err) } if err := msgs.Create(ctx, &msgentity.Message{ ID: msgID, ConversationID: convID, SenderID: 1, ClientMsgID: "t1", Type: msgentity.TypeText, Content: raw, Status: msgentity.StatusNormal, }); err != nil { t.Fatal(err) } if _, err := svc.Create(ctx, 9, &CreateInput{MsgID: msgID}); !errors.Is(err, errs.ErrForbidden) { t.Fatalf("member %v", err) } }