thread-master/apps/backend/internal/module/growth/usecase/service_test.go

246 lines
8.8 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"
"errors"
"testing"
"apps/backend/internal/module/growth/domain"
"apps/backend/internal/module/growth/repository"
"github.com/stretchr/testify/require"
)
func TestOC_01_CreateOnPublished(t *testing.T) {
svc := New(repository.NewMemory())
const originalSentAt int64 = 1_700_000_000_000_000_000
e, err := svc.RecordPublished(context.Background(), 1001, domain.SourceScoutOutreach, "post1", "acc1", originalSentAt)
require.NoError(t, err)
require.Equal(t, domain.StatusObserving, e.Status)
require.Equal(t, domain.KindReach, e.Kind)
// idempotent
e2, err := svc.RecordPublished(context.Background(), 1001, domain.SourceScoutOutreach, "post1", "acc1", originalSentAt+1_000_000)
require.NoError(t, err)
require.Equal(t, e.ID, e2.ID)
require.Equal(t, originalSentAt, e2.SentAt)
}
func TestOC_02_03_ApplySignals(t *testing.T) {
svc := New(repository.NewMemory())
e, _ := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p", "a", 0)
e, err := svc.ApplyObserveSignals(context.Background(), e.ID, 1, 0, 0)
require.NoError(t, err)
require.Equal(t, domain.ConfidenceConfirmed, e.Confidence)
require.Equal(t, domain.KindReply, e.Kind)
e3, _ := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p2", "a", 0)
e3, err = svc.ApplyObserveSignals(context.Background(), e3.ID, 0, 0, 2)
require.NoError(t, err)
require.Equal(t, domain.ConfidencePossible, e3.Confidence)
sum, err := svc.Summary(context.Background(), 1, 0, 0)
require.NoError(t, err)
require.Equal(t, 2, sum.FollowsPossible)
}
func TestOC_04_05_Conversion(t *testing.T) {
svc := New(repository.NewMemory())
e, _ := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p", "a", 0)
e, err := svc.ReportConversion(context.Background(), 1, e.ID, 1000, "成交", "TWD")
require.NoError(t, err)
require.True(t, e.HasConversion)
require.Equal(t, domain.KindConversion, e.Kind)
sum, err := svc.Summary(context.Background(), 1, 0, 0)
require.NoError(t, err)
require.Equal(t, 1, sum.Conversions)
require.Equal(t, 1000.0, sum.ConversionAmount)
require.NoError(t, svc.DeleteConversion(context.Background(), 1, e.ID))
sum, _ = svc.Summary(context.Background(), 1, 0, 0)
require.Equal(t, 0, sum.Conversions)
}
func TestOC_08_DegradedStillWorks(t *testing.T) {
svc := New(repository.NewMemory())
require.Equal(t, "degraded", svc.SignalProbe)
e, err := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p", "", 0)
require.NoError(t, err)
_, err = svc.ReportConversion(context.Background(), 1, e.ID, 100, "", "")
require.NoError(t, err)
sum, err := svc.Summary(context.Background(), 1, 0, 0)
require.NoError(t, err)
require.Equal(t, 1, sum.Reach)
}
func TestCU_01_02_03_GenerateCheckup(t *testing.T) {
svc := New(repository.NewMemory())
_, _ = svc.RecordPublished(context.Background(), 9, domain.SourceScoutOutreach, "x", "a", 0)
c, err := svc.GenerateCheckup(context.Background(), 9, "Asia/Taipei", false)
require.NoError(t, err)
require.Equal(t, domain.CheckupReady, c.Status)
require.Len(t, c.Actions, 3)
for _, f := range c.Findings {
require.NotEmpty(t, f.Evidence)
}
// first force ok (total gens=2); third gen in 7d window blocked
_, err = svc.GenerateCheckup(context.Background(), 9, "Asia/Taipei", true)
require.NoError(t, err)
_, err = svc.GenerateCheckup(context.Background(), 9, "Asia/Taipei", true)
require.ErrorIs(t, err, domain.ErrRateLimited)
}
func TestAH_01_03_HealthGate(t *testing.T) {
svc := New(repository.NewMemory())
h, err := svc.RecomputeHealth(context.Background(), 1, "acc", HealthSignals{
Sends24h: 30, IntervalViolations: 5, FailRate7d: 0.6, TokenBad: true,
})
require.NoError(t, err)
require.Equal(t, domain.HealthThrottle, h.Level)
_, err = svc.EnsureAutoSendAllowed(context.Background(), 1, "acc")
require.ErrorIs(t, err, domain.ErrThrottle)
h2, err := svc.RecomputeHealth(context.Background(), 1, "acc2", HealthSignals{
Sends24h: 15, IntervalViolations: 2, FailRate7d: 0.3,
})
require.NoError(t, err)
require.Equal(t, domain.HealthWarn, h2.Level)
warn, err := svc.EnsureAutoSendAllowed(context.Background(), 1, "acc2")
require.NoError(t, err)
require.NotEmpty(t, warn)
}
func TestIR_01_03_InviteReward(t *testing.T) {
svc := New(repository.NewMemory())
svc.PlanQuota = func(id string) int {
if id == "@inviter" {
return 2000 // pro inviter → month cap 1000
}
return 120
}
r, err := svc.OnFirstPaidPlan(context.Background(), 2, 1, "starter")
require.NoError(t, err)
require.Equal(t, domain.RewardCredited, r.Status)
require.Equal(t, 100, r.Points)
// second time skipped
r2, err := svc.OnFirstPaidPlan(context.Background(), 2, 1, "pro")
require.NoError(t, err)
require.Equal(t, domain.RewardSkipped, r2.Status)
// pro first
r3, err := svc.OnFirstPaidPlan(context.Background(), 3, 1, "pro")
require.NoError(t, err)
require.Equal(t, 300, r3.Points)
}
func TestWS_01_02_Workspace(t *testing.T) {
svc := New(repository.NewMemory())
list, cur, err := svc.ListWorkspaces(context.Background(), 5)
require.NoError(t, err)
require.Len(t, list, 1)
require.NotEmpty(t, cur)
w2, err := svc.CreateWorkspace(context.Background(), 5, "客戶 A")
require.NoError(t, err)
require.NoError(t, svc.SwitchWorkspace(context.Background(), 5, w2.ID))
list, cur, err = svc.ListWorkspaces(context.Background(), 5)
require.NoError(t, err)
require.Equal(t, w2.ID, cur)
require.Len(t, list, 2)
}
func TestWS_03_04_ReviewGate(t *testing.T) {
svc := New(repository.NewMemory())
list, _, _ := svc.ListWorkspaces(context.Background(), 7)
ws := list[0]
on := true
_, err := svc.UpdateWorkspace(context.Background(), 7, ws.ID, "", &on)
require.NoError(t, err)
err = svc.EnsureReviewAllowsAutoSend(context.Background(), 7, ws.ID, "play", "p1")
require.True(t, errors.Is(err, domain.ErrReviewPending))
r, err := svc.SubmitReview(context.Background(), 7, "play", "p1", ws.ID)
require.NoError(t, err)
_, err = svc.DecideReview(context.Background(), 7, r.ID, domain.ReviewApproved, "")
require.NoError(t, err)
require.NoError(t, svc.EnsureReviewAllowsAutoSend(context.Background(), 7, ws.ID, "play", "p1"))
}
func TestWS_05_ExportReport(t *testing.T) {
svc := New(repository.NewMemory())
list, _, _ := svc.ListWorkspaces(context.Background(), 8)
fn, body, err := svc.ExportReportMarkdown(context.Background(), 8, list[0].ID, "2026-07")
require.NoError(t, err)
require.Contains(t, fn, "2026-07")
require.Contains(t, body, "月報")
}
// /u/{code} 是免登入 302任何存進去的 destination 都會變成本站可用的跳板。
// 舊檢查只看 "http" 前綴,"httpx://" 與 "//evil" 都能過。
func TestUTM_RejectsDestinationsThatAreNotHttpURLs(t *testing.T) {
svc := New(repository.NewMemory())
ctx := context.Background()
for _, dest := range []string{
"javascript:alert(1)",
"httpx://evil.example",
"//evil.example/path",
"http://",
"data:text/html,<script>alert(1)</script>",
"",
" ",
} {
_, err := svc.CreateUtmLink(ctx, 1, dest, "", "label")
require.ErrorIsf(t, err, domain.ErrValidation, "destination %q should be rejected", dest)
}
}
func TestUTM_AcceptsHttpAndHttpsAndRedirectsThere(t *testing.T) {
svc := New(repository.NewMemory())
ctx := context.Background()
for _, dest := range []string{
"https://shop.example/product?utm_source=threads",
"http://shop.example",
} {
link, err := svc.CreateUtmLink(ctx, 1, dest, "", "label")
require.NoError(t, err)
got, err := svc.TrackUtmClick(ctx, link.Code)
require.NoError(t, err)
require.Equal(t, dest, got)
}
}
// 這個檢查是後來才加的,先前寫進 DB 的列可能還帶著壞值,所以出門前要再驗一次。
func TestUTM_RefusesToRedirectToAStoredBadDestination(t *testing.T) {
repo := repository.NewMemory()
svc := New(repo)
ctx := context.Background()
link, err := svc.CreateUtmLink(ctx, 1, "https://shop.example", "", "label")
require.NoError(t, err)
link.DestinationURL = "javascript:alert(1)"
require.NoError(t, repo.SaveUtmLink(ctx, link))
_, err = svc.TrackUtmClick(ctx, link.Code)
require.ErrorIs(t, err, domain.ErrValidation)
}
// 互動率是比率,走 Round2 會被輾成整數百分比0.0234 → 0.02 → 前端 2.00%)。
func TestRoundRateKeepsTwoDecimalsOfAPercentage(t *testing.T) {
require.Equal(t, 0.0234, RoundRate(0.023449))
require.Equal(t, 0.1235, RoundRate(0.12345))
require.Equal(t, 0.0, RoundRate(0))
require.Equal(t, 0.02, Round2(0.023449), "Round2 仍供瀏覽數這類整數量使用")
}
func TestBenchmarkBucketsIgnoreCaseAndPadding(t *testing.T) {
svc := New(repository.NewMemory())
ctx := context.Background()
for uid := int64(1); uid <= 5; uid++ {
require.NoError(t, svc.ContributeBenchmark(ctx, uid, " SkinCare ", 0.02, 100))
}
size, medEng, medViews, available, _ := svc.GetBenchmark(ctx, "skincare", 0.02, 100)
require.True(t, available)
require.Equal(t, 5, size)
require.Equal(t, 0.02, medEng)
require.Equal(t, 100.0, medViews)
}