thread-master/apps/backend/internal/module/radar/usecase/reply_send_test.go

201 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"context"
"fmt"
"strings"
"testing"
"apps/backend/internal/module/radar/domain"
"apps/backend/internal/module/radar/repository"
)
type fakeHealth struct{ level, advice string }
func (f fakeHealth) WorstLevel(context.Context, int64) (string, string, error) {
return f.level, f.advice, nil
}
func TestMarkReplyUsed_ManualCopyAndThrottle(t *testing.T) {
ctx := context.Background()
mem := repository.NewMemory()
svc := New(mem)
uid := int64(3)
now := domain.NowNano()
o := &domain.Opportunity{
ID: "o1", OwnerUID: uid, ExternalID: "e1", Permalink: "https://x/e1",
AuthorHandle: "a", Text: "需要幫忙", PostedAt: now, Status: domain.OppQualified,
IntentScore: 80, IntentBand: domain.BandHigh,
Reasons: []domain.OpportunityReason{
{Dimension: domain.DimAuthenticity, Score: 20, Reason: "a"},
{Dimension: domain.DimIntent, Score: 20, Reason: "i"},
{Dimension: domain.DimRegion, Score: 10, Reason: "r"},
{Dimension: domain.DimFreshness, Score: 15, Reason: "f"},
{Dimension: domain.DimFit, Score: 15, Reason: "fit"},
},
RegionMatch: domain.RegionUnknown, MatchedTerms: []string{"x"}, CreatedAt: now, UpdatedAt: now,
}
if _, err := mem.UpsertByExternalID(ctx, o); err != nil {
t.Fatal(err)
}
pub := &domain.ReplyVariant{
ID: "r1", OwnerUID: uid, OpportunityID: "o1", Variant: domain.ReplyPublicComment,
Text: "嗨", CreatedAt: now,
}
dm := &domain.ReplyVariant{
ID: "r2", OwnerUID: uid, OpportunityID: "o1", Variant: domain.ReplyDM,
Text: "私訊", CreatedAt: now,
}
if err := mem.SaveReply(ctx, pub); err != nil {
t.Fatal(err)
}
if err := mem.SaveReply(ctx, dm); err != nil {
t.Fatal(err)
}
got, _, err := svc.MarkReplyUsed(ctx, uid, "o1", "r1", domain.SentManualCopy, "")
if err != nil {
t.Fatal(err)
}
if got.UsedAt == 0 || got.SentChannel != domain.SentManualCopy {
t.Fatalf("manual mark: %+v", got)
}
if _, _, err := svc.MarkReplyUsed(ctx, uid, "o1", "r2", domain.SentOutbox, ""); err == nil {
t.Fatal("dm outbox should fail")
}
svc.Health = fakeHealth{level: "throttle", advice: "慢一點"}
_, _, err = svc.MarkReplyUsed(ctx, uid, "o1", "r1", domain.SentOutbox, "")
// r1 already used; still exercise throttle on a fresh reply
pub2 := &domain.ReplyVariant{
ID: "r3", OwnerUID: uid, OpportunityID: "o1", Variant: domain.ReplyPublicComment,
Text: "再一則", CreatedAt: now,
}
_ = mem.SaveReply(ctx, pub2)
_, advice, err := svc.MarkReplyUsed(ctx, uid, "o1", "r3", domain.SentOutbox, "")
if err == nil {
t.Fatal("expected throttle block")
}
if !strings.Contains(err.Error(), "throttle") {
t.Fatalf("err = %v", err)
}
if advice != "慢一點" {
t.Fatalf("advice = %q", advice)
}
}
type fakeReplyQueue struct {
outboxID string
err error
calls []queuedReply
}
type queuedReply struct {
ownerUID int64
accountID string
mediaID string
text string
title string
}
func (f *fakeReplyQueue) QueueExternalReply(_ context.Context, ownerUID int64, accountID, replyToMediaID, text, title string) (string, error) {
f.calls = append(f.calls, queuedReply{ownerUID, accountID, replyToMediaID, text, title})
if f.err != nil {
return "", f.err
}
if f.outboxID != "" {
return f.outboxID, nil
}
return "outbox-1", nil
}
type fakeMediaResolver struct {
mediaID string
err error
}
func (f fakeMediaResolver) ResolveMediaID(_ context.Context, _ int64, _ string) (string, error) {
if f.err != nil {
return "", f.err
}
return f.mediaID, nil
}
// 接上 ReplyQueue 後outbox 標記要真的排入既有 Outbox用解析後的 media id、
// 帳號來自呼叫端),且把回傳的 outbox_id 存回 reply 供前端顯示。
func TestMarkReplyUsed_OutboxRealSend(t *testing.T) {
ctx := context.Background()
mem := repository.NewMemory()
svc := New(mem)
uid := int64(3)
now := domain.NowNano()
o := &domain.Opportunity{
ID: "o1", OwnerUID: uid, ExternalID: "https://threads.net/@a/post/e1", Permalink: "https://threads.net/@a/post/e1",
AuthorHandle: "a", Text: "需要幫忙", PostedAt: now, Status: domain.OppQualified,
IntentScore: 80, IntentBand: domain.BandHigh,
Reasons: []domain.OpportunityReason{
{Dimension: domain.DimAuthenticity, Score: 20, Reason: "a"},
{Dimension: domain.DimIntent, Score: 20, Reason: "i"},
{Dimension: domain.DimRegion, Score: 10, Reason: "r"},
{Dimension: domain.DimFreshness, Score: 15, Reason: "f"},
{Dimension: domain.DimFit, Score: 15, Reason: "fit"},
},
RegionMatch: domain.RegionUnknown, MatchedTerms: []string{"x"}, CreatedAt: now, UpdatedAt: now,
}
if _, err := mem.UpsertByExternalID(ctx, o); err != nil {
t.Fatal(err)
}
pub := &domain.ReplyVariant{
ID: "r1", OwnerUID: uid, OpportunityID: "o1", Variant: domain.ReplyPublicComment,
Text: "嗨", CreatedAt: now,
}
if err := mem.SaveReply(ctx, pub); err != nil {
t.Fatal(err)
}
queue := &fakeReplyQueue{outboxID: "outbox-42"}
svc.ReplyQueue = queue
svc.MediaResolver = fakeMediaResolver{mediaID: "999888777"}
// 沒帶 account_id 要明確拒絕,不能默默送到不存在的帳號。
if _, _, err := svc.MarkReplyUsed(ctx, uid, "o1", "r1", domain.SentOutbox, ""); !strings.Contains(err.Error(), "account_id") {
t.Fatalf("err = %v, want account_id required", err)
}
got, _, err := svc.MarkReplyUsed(ctx, uid, "o1", "r1", domain.SentOutbox, "acc-1")
if err != nil {
t.Fatalf("mark used: %v", err)
}
if got.OutboxID != "outbox-42" {
t.Fatalf("outbox_id = %q, want outbox-42", got.OutboxID)
}
if len(queue.calls) != 1 {
t.Fatalf("QueueExternalReply calls = %d, want 1", len(queue.calls))
}
call := queue.calls[0]
if call.accountID != "acc-1" || call.mediaID != "999888777" || call.text != "嗨" {
t.Fatalf("queued call = %+v", call)
}
// 佇列失敗要整體失敗,不能標記已用卻沒真的送出。
pub2 := &domain.ReplyVariant{
ID: "r2", OwnerUID: uid, OpportunityID: "o1", Variant: domain.ReplyPublicComment,
Text: "再一則", CreatedAt: now,
}
_ = mem.SaveReply(ctx, pub2)
queue.err = fmt.Errorf("threads api down")
if _, _, err := svc.MarkReplyUsed(ctx, uid, "o1", "r2", domain.SentOutbox, "acc-1"); err == nil {
t.Fatal("expected queue failure to propagate")
}
after, gerr := mem.GetReply(ctx, "r2")
if gerr != nil {
t.Fatalf("get reply: %v", gerr)
}
if after.UsedAt != 0 {
t.Fatalf("reply should not be marked used when the queue call failed: %+v", after)
}
}