feat(squarebot): 补充 Bot Worker 与单测
完善广场机器人定时 Worker,并补充 RSS 解析相关单测。
This commit is contained in:
parent
f708a86d5c
commit
e1b3a7ebd2
135
internal/module/squarebot/logic/service_test.go
Normal file
135
internal/module/squarebot/logic/service_test.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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 & World</title>
|
||||||
|
<link>https://example.com/a</link>
|
||||||
|
<description><p>Short summary here.</p></description>
|
||||||
|
</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])
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
internal/module/squarebot/logic/worker.go
Normal file
69
internal/module/squarebot/logic/worker.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Worker runs Tick on an interval until Stop.
|
||||||
|
type Worker struct {
|
||||||
|
log *zap.Logger
|
||||||
|
svc *Service
|
||||||
|
stop chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorker(log *zap.Logger, svc *Service) *Worker {
|
||||||
|
if log == nil {
|
||||||
|
log = zap.NewNop()
|
||||||
|
}
|
||||||
|
return &Worker{log: log, svc: svc, stop: make(chan struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) Start() {
|
||||||
|
if w == nil || w.svc == nil || !w.svc.Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.wg.Add(1)
|
||||||
|
go w.loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) Stop() {
|
||||||
|
if w == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-w.stop:
|
||||||
|
default:
|
||||||
|
close(w.stop)
|
||||||
|
}
|
||||||
|
w.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) loop() {
|
||||||
|
defer w.wg.Done()
|
||||||
|
interval := w.svc.Interval()
|
||||||
|
// Run once shortly after boot so ops can verify without waiting a full interval.
|
||||||
|
timer := time.NewTimer(15 * time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-w.stop:
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
w.runOnce()
|
||||||
|
timer.Reset(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) runOnce() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := w.svc.Tick(ctx); err != nil {
|
||||||
|
w.log.Warn("squarebot tick", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user