Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout. Co-authored-by: Cursor <cursoragent@cursor.com>
137 lines
3.7 KiB
Go
137 lines
3.7 KiB
Go
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"
|
|
objs.keys[src] = true
|
|
raw, err := msgentity.NewImage(&msgentity.Image{Key: src, 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/1/") {
|
|
t.Fatalf("%+v", item)
|
|
}
|
|
if strings.Contains(string(item.Content), src) {
|
|
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)
|
|
}
|
|
}
|