spark-server/internal/module/squarebot/logic/service_test.go
2026-09-29 10:22:28 +08:00

165 lines
4.6 KiB
Go

package logic
import (
"context"
"errors"
"strings"
"testing"
"time"
"spark-server/internal/infrastructure/id"
friendlogic "spark-server/internal/module/friend/logic"
friendmodel "spark-server/internal/module/friend/model"
squarelogic "spark-server/internal/module/square/logic"
squaremodel "spark-server/internal/module/square/model"
userentity "spark-server/internal/module/user/entity"
"spark-server/internal/module/user/logictest"
"spark-server/internal/pkg/errs"
)
func TestParseRSSAndFormat(t *testing.T) {
body := []byte(`<?xml version="1.0"?>
<rss version="2.0"><channel>
<title>Demo News</title>
<item>
<title>Hello &amp; World</title>
<link>https://example.com/a</link>
<description>&lt;p&gt;Short summary here.&lt;/p&gt;&lt;img src="https://example.com/pic.jpg"/&gt;</description>
<enclosure url="https://example.com/cover.jpg" type="image/jpeg"/>
</item>
</channel></rss>`)
items, err := parseFeed(body, "https://example.com/rss")
if err != nil || len(items) != 1 {
t.Fatalf("%+v %v", items, err)
}
if items[0].Title != "Hello & World" || items[0].Link != "https://example.com/a" {
t.Fatalf("%+v", items[0])
}
if items[0].ImageURL != "https://example.com/cover.jpg" {
t.Fatalf("image=%q", items[0].ImageURL)
}
text := FormatPostText(&items[0])
if !strings.Contains(text, "Demo News") || !strings.Contains(text, "https://example.com/a") {
t.Fatalf("text=%q", text)
}
}
func TestPublishBotMediaTTLAndDedupe(t *testing.T) {
ids, err := id.New(2)
if err != nil {
t.Fatal(err)
}
users := logictest.Users(ids, nil)
friends := friendlogic.NewService(friendmodel.NewMemoryRepo(), users, ids, nil)
square := squarelogic.NewService(squaremodel.NewMemoryRepo(), friends, users, nil, ids)
ctx := t.Context()
u, err := users.AuthenticateEmail(ctx, "test01@spark.com", "1", &userentity.LoginDevice{
DeviceID: 9, DeviceType: userentity.DeviceIOS,
})
if err != nil {
t.Fatal(err)
}
in := &squarelogic.BotPublishInput{
Text: "title\n\nsum\n\nhttps://example.com/x", SourceURL: "https://example.com/x", TTL: time.Hour,
}
v, err := square.PublishBotMedia(ctx, u.ID, in)
if err != nil || v == nil {
t.Fatalf("%v %v", v, err)
}
_, err = square.PublishBotMedia(ctx, u.ID, in)
if !errors.Is(err, errs.ErrDuplicate) {
t.Fatalf("want duplicate got %v", err)
}
feed, err := square.Feed(ctx, u.ID, 0, 20)
if err != nil || len(feed.Items) < 1 {
t.Fatalf("feed %+v %v", feed, err)
}
n, err := square.ExpireBotPosts(ctx, time.Now().Add(2*time.Hour), 10)
if err != nil || n < 1 {
t.Fatalf("expire %d %v", n, err)
}
feed2, err := square.Feed(ctx, u.ID, 0, 20)
if err != nil {
t.Fatal(err)
}
for _, it := range feed2.Items {
if it.ID == v.ID {
t.Fatal("expired post still visible")
}
}
}
func TestServiceDisabledWithoutDeps(t *testing.T) {
svc := NewService(nil, nil, nil, nil, &Options{Enabled: true})
if svc.Enabled() {
t.Fatal("enabled without deps")
}
}
type stubUsers struct {
u *userentity.User
}
func (s stubUsers) GetByEmail(context.Context, string) (*userentity.User, error) {
if s.u == nil {
return nil, errs.ErrNotFound
}
return s.u, nil
}
type stubSquare struct {
published int
expired int
}
func (s *stubSquare) PublishBotMedia(context.Context, int64, *squarelogic.BotPublishInput) (*squarelogic.View, error) {
s.published++
return &squarelogic.View{ID: 1}, nil
}
func (s *stubSquare) ExpireBotPosts(context.Context, time.Time, int) (int, error) {
s.expired++
return 0, nil
}
func TestTickExpiresEvenIfFeedFails(t *testing.T) {
sq := &stubSquare{}
users := stubUsers{u: &userentity.User{ID: 7, Nickname: "Bot"}}
svc := NewService(nil, sq, users, nil, &Options{
Enabled: true, Interval: time.Minute, TTL: time.Hour, BotIndexes: []int{1},
})
svc.opts.Feeds = []string{"http://127.0.0.1:1/nope"}
err := svc.Tick(t.Context())
if err == nil {
t.Fatal("expected feed error")
}
if sq.expired != 1 {
t.Fatalf("expire called %d", sq.expired)
}
}
type skipLock struct{}
func (skipLock) TryLock(context.Context, string, time.Duration) (bool, error) {
return false, nil
}
func TestTickSkipsWhenLockHeld(t *testing.T) {
sq := &stubSquare{}
users := stubUsers{u: &userentity.User{ID: 7, Nickname: "Bot"}}
svc := NewService(nil, sq, users, skipLock{}, &Options{Enabled: true, BotIndexes: []int{1}})
if err := svc.Tick(t.Context()); err != nil {
t.Fatal(err)
}
if sq.published != 0 || sq.expired != 0 {
t.Fatalf("want skip publish=%d expire=%d", sq.published, sq.expired)
}
}
func TestServiceInterval(t *testing.T) {
svc := NewService(nil, nil, nil, nil, &Options{Interval: 2 * time.Minute})
if svc.Interval() != 2*time.Minute {
t.Fatalf("interval %v", svc.Interval())
}
}