owpengram-server/internal/store/postgres/moderation_integration_test.go
2026-07-24 11:56:59 +08:00

471 lines
16 KiB
Go

package postgres
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
moderationapp "telesrv/internal/app/moderation"
"telesrv/internal/domain"
)
func TestModerationReportStoreAtomicEvidenceAndIdempotency(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
now := time.Now().UTC()
reporter := now.UnixNano()&0x3fffffff + 5_000
report, err := domain.NewModerationReport(domain.ModerationReportDraft{
ReporterUserID: reporter, Source: domain.ModerationSourceProfilePhoto,
Target: domain.Peer{Type: domain.PeerTypeUser, ID: reporter + 1},
Reason: domain.ModerationReasonFake, Option: "v1/fake",
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemProfilePhoto,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: reporter + 1},
ItemID: reporter + 2, AuthorUserID: reporter + 1,
EvidenceSchemaVersion: 1,
Evidence: []byte(`{"photo_id":2,"owner_id":1}`),
}},
MediaHolds: []domain.ModerationMediaHold{{
ItemIndex: 0, Kind: domain.ModerationMediaPhoto,
StorageKey: "profile/photo/test",
}},
CreatedAt: now,
})
if err != nil {
t.Fatal(err)
}
store := NewModerationReportStore(pool)
stored, created, err := store.CreateModerationReport(ctx, report)
if err != nil || !created {
t.Fatalf("create=%v err=%v", created, err)
}
t.Cleanup(func() {
cleanupModerationReport(t, pool, stored.ID)
})
retry, created, err := store.CreateModerationReport(ctx, report)
if err != nil || created || retry.ID != stored.ID {
t.Fatalf("retry=%+v created=%v err=%v", retry, created, err)
}
got, found, err := store.GetModerationReport(ctx, stored.ID)
if err != nil || !found {
t.Fatalf("get found=%v err=%v", found, err)
}
if got.Fingerprint != report.Fingerprint || len(got.Items) != 1 ||
len(got.MediaHolds) != 1 || got.MediaHolds[0].StorageKey != "profile/photo/test" {
t.Fatalf("stored report = %+v", got)
}
var reports, items, holds int
if err := pool.QueryRow(ctx, `
SELECT
(SELECT count(*) FROM moderation_reports WHERE id = $1),
(SELECT count(*) FROM moderation_report_items WHERE report_id = $1),
(SELECT count(*) FROM moderation_media_holds WHERE report_id = $1)`,
stored.ID).Scan(&reports, &items, &holds); err != nil {
t.Fatal(err)
}
if reports != 1 || items != 1 || holds != 1 {
t.Fatalf("rows reports=%d items=%d holds=%d", reports, items, holds)
}
}
func TestModerationSponsoredReportIsAtomicUnderConcurrentFinalOptions(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
now := time.Now().UTC()
userID := now.UnixNano()&0x3fffffff + 8_000
randomID := []byte("postgres-sponsored-random-id")
store := NewModerationReportStore(pool)
impression, err := domain.NewSponsoredMessageImpression(
userID, randomID,
domain.Peer{Type: domain.PeerTypeChannel, ID: userID + 1},
userID+2, []byte(`{"creative_id":"pg-creative","schema_version":1}`),
now, now.Add(time.Hour),
)
if err != nil {
t.Fatal(err)
}
impression, created, err := store.CreateSponsoredMessageImpression(ctx, impression)
if err != nil || !created {
t.Fatalf("impression=%+v created=%v err=%v", impression, created, err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM sponsored_message_impressions WHERE id = $1", impression.ID)
})
service := moderationapp.NewService(store)
type result struct {
report domain.ModerationReport
created bool
err error
}
start := make(chan struct{})
results := make(chan result, 2)
var wg sync.WaitGroup
for _, option := range []struct {
reason domain.ModerationReason
option string
}{
{domain.ModerationReasonSpam, "spam"},
{domain.ModerationReasonFake, "fake"},
} {
wg.Add(1)
go func(reason domain.ModerationReason, option string) {
defer wg.Done()
<-start
report, created, err := service.ReportSponsored(
ctx, userID, randomID, reason, option, now.Add(time.Second),
)
results <- result{report: report, created: created, err: err}
}(option.reason, option.option)
}
close(start)
wg.Wait()
close(results)
var reportID int64
var createdCount int
for got := range results {
if got.err != nil || got.report.ID <= 0 {
t.Fatalf("concurrent result=%+v", got)
}
if reportID == 0 {
reportID = got.report.ID
} else if got.report.ID != reportID {
t.Fatalf("report ids differ: %d vs %d", reportID, got.report.ID)
}
if got.created {
createdCount++
}
}
if createdCount != 1 {
t.Fatalf("created count=%d, want 1", createdCount)
}
t.Cleanup(func() { cleanupModerationReport(t, pool, reportID) })
var reportCount int
if err := pool.QueryRow(ctx, `
SELECT count(*)
FROM moderation_reports
WHERE reporter_user_id = $1 AND source = 'sponsored'`,
userID,
).Scan(&reportCount); err != nil {
t.Fatal(err)
}
if reportCount != 1 {
t.Fatalf("sponsored reports=%d, want 1", reportCount)
}
}
func TestModerationCaseActionAppealLinkAndTelemetryPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
now := time.Now().UTC()
reporter := now.UnixNano()&0x3fffffff + 12_000
target := domain.Peer{Type: domain.PeerTypeUser, ID: reporter + 1}
store := NewModerationReportStore(pool)
service := moderationapp.NewService(store)
report, err := domain.NewModerationReport(domain.ModerationReportDraft{
ReporterUserID: reporter, Source: domain.ModerationSourceAccountPeer,
Target: target, Reason: domain.ModerationReasonFake, Option: "fake",
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemPeer, Peer: target, ItemID: target.ID,
AuthorUserID: target.ID, EvidenceSchemaVersion: 1,
Evidence: []byte(`{"schema_version":1}`),
}},
CreatedAt: now,
})
if err != nil {
t.Fatal(err)
}
stored, created, err := store.CreateModerationReport(ctx, report)
if err != nil || !created {
t.Fatalf("report=%+v created=%v err=%v", stored, created, err)
}
t.Cleanup(func() { cleanupModerationReport(t, pool, stored.ID) })
cases, err := store.ListModerationCases(ctx, domain.ModerationCaseFilter{
Target: target, Limit: 10,
})
if err != nil || len(cases) != 1 {
t.Fatalf("cases=%+v err=%v", cases, err)
}
claimed, err := store.ClaimModerationCase(
ctx, cases[0].ID, cases[0].Version, "pg-reviewer", now.Add(time.Second),
)
if err != nil {
t.Fatal(err)
}
decision, err := domain.NewModerationDecisionRequest(domain.ModerationDecisionRequest{
CaseID: claimed.ID, ExpectedVersion: claimed.Version,
Actor: "pg-reviewer", Reason: "confirmed fake",
CommandID: "pg-moderation-decision-" + time.Unix(0, reporter).Format("150405.000000000"),
Kind: domain.ModerationDecisionViolation,
Actions: []domain.ModerationActionDraft{{
Kind: domain.ModerationActionMarkFake, Payload: []byte(`{}`),
}},
CreatedAt: now.Add(2 * time.Second),
})
if err != nil {
t.Fatal(err)
}
if _, created, err := store.DecideModerationCase(ctx, decision); err != nil || !created {
t.Fatalf("decision created=%v err=%v", created, err)
}
actions, err := store.ClaimModerationActions(
ctx, now.Add(3*time.Second), 10, time.Minute,
)
if err != nil || len(actions) != 1 {
t.Fatalf("actions=%+v err=%v", actions, err)
}
if err := store.CompleteModerationAction(
ctx, actions[0].ID, actions[0].Attempts, true, "",
time.Time{}, now.Add(4*time.Second),
); err != nil {
t.Fatal(err)
}
token, err := service.IssueAppealLink(
ctx, cases[0].ID, target.ID, now.Add(time.Hour), now.Add(5*time.Second),
)
if err != nil {
t.Fatal(err)
}
appeal, created, err := service.SubmitAppealLink(
ctx, token, "Postgres appeal.", now.Add(6*time.Second),
)
if err != nil || !created || appeal.ID <= 0 {
t.Fatalf("appeal=%+v created=%v err=%v", appeal, created, err)
}
retry, created, err := service.SubmitAppealLink(
ctx, token, "retry body", now.Add(7*time.Second),
)
if err != nil || created || retry.ID != appeal.ID ||
retry.Text != appeal.Text {
t.Fatalf("appeal retry=%+v created=%v err=%v", retry, created, err)
}
telemetryStore := NewClientTelemetryStore(pool)
telemetryAt := time.Unix(reporter%1_000_000+1, 0).UTC()
event, err := domain.NewClientTelemetryEvent(
reporter, domain.ClientTelemetryMessageDelivery, target,
[]int64{3, 1, 2}, map[string]any{"push": true}, telemetryAt,
)
if err != nil {
t.Fatal(err)
}
telemetry, created, err := telemetryStore.CreateClientTelemetry(ctx, event)
if err != nil || !created || telemetry.ID <= 0 {
t.Fatalf("telemetry=%+v created=%v err=%v", telemetry, created, err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM client_telemetry_events WHERE id = $1", telemetry.ID)
})
retryTelemetry, created, err := telemetryStore.CreateClientTelemetry(ctx, event)
if err != nil || created || retryTelemetry.ID != telemetry.ID {
t.Fatalf("telemetry retry=%+v created=%v err=%v", retryTelemetry, created, err)
}
deleted, err := telemetryStore.DeleteExpiredClientTelemetry(
ctx, telemetryAt.Add(time.Second), 10,
)
if err != nil || deleted < 1 {
t.Fatalf("telemetry retention deleted=%d err=%v", deleted, err)
}
}
func TestModerationSanctionSupersessionAndAppealOwnershipPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
now := time.Now().UTC()
store := NewModerationReportStore(pool)
service := moderationapp.NewService(store)
base := now.UnixNano()&0x3fffffff + 40_000
createDecision := func(target domain.Peer, reporter int64, option, command string, at time.Time) (int64, int64) {
t.Helper()
report, _, err := service.AcceptReport(ctx, domain.ModerationReportDraft{
ReporterUserID: reporter, Source: domain.ModerationSourceAccountPeer,
Target: target, Reason: domain.ModerationReasonFake, Option: option,
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemPeer, Peer: target, ItemID: target.ID,
AuthorUserID: target.ID, EvidenceSchemaVersion: 1,
Evidence: []byte(`{"schema_version":1}`),
}},
CreatedAt: at,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { cleanupModerationReport(t, pool, report.ID) })
cases, err := service.ListCases(ctx, domain.ModerationCaseFilter{
Statuses: []domain.ModerationCaseStatus{domain.ModerationCaseOpen},
Target: target, Limit: 10,
})
if err != nil || len(cases) != 1 {
t.Fatalf("open cases=%+v err=%v", cases, err)
}
claimed, err := service.ClaimCase(
ctx, cases[0].ID, cases[0].Version, "pg-owner", at.Add(time.Second),
)
if err != nil {
t.Fatal(err)
}
detail, _, err := service.DecideCase(ctx, domain.ModerationDecisionRequest{
CaseID: claimed.ID, ExpectedVersion: claimed.Version,
Actor: "pg-owner", Reason: "confirmed", CommandID: command,
Kind: domain.ModerationDecisionViolation,
Actions: []domain.ModerationActionDraft{{
Kind: domain.ModerationActionMarkFake, Payload: []byte(`{}`),
}},
CreatedAt: at.Add(2 * time.Second),
})
if err != nil || len(detail.Actions) != 1 {
t.Fatalf("decision=%+v err=%v", detail, err)
}
return claimed.ID, detail.Actions[0].ID
}
target := domain.Peer{Type: domain.PeerTypeUser, ID: base + 1}
oldCaseID, oldActionID := createDecision(target, base+2, "old", "pg-old", now)
newCaseID, newActionID := createDecision(target, base+3, "new", "pg-new", now.Add(3*time.Second))
claimedActions, err := store.ClaimModerationActions(ctx, now.Add(6*time.Second), 10, time.Minute)
if err != nil {
t.Fatal(err)
}
claimedByID := make(map[int64]domain.ModerationAction, len(claimedActions))
for _, action := range claimedActions {
claimedByID[action.ID] = action
}
oldAction, oldFound := claimedByID[oldActionID]
newAction, newFound := claimedByID[newActionID]
if !oldFound || !newFound {
t.Fatalf("claimed actions=%+v", claimedActions)
}
if current, err := store.IsModerationActionCurrent(ctx, oldAction); err != nil || current {
t.Fatalf("old current=%v err=%v", current, err)
}
if current, err := store.IsModerationActionCurrent(ctx, newAction); err != nil || !current {
t.Fatalf("new current=%v err=%v", current, err)
}
if err := store.SupersedeModerationAction(
ctx, oldAction.ID, oldAction.Attempts, now.Add(7*time.Second),
); err != nil {
t.Fatal(err)
}
if err := store.CompleteModerationAction(
ctx, newAction.ID, newAction.Attempts, true, "", time.Time{},
now.Add(8*time.Second),
); err != nil {
t.Fatal(err)
}
oldDetail, _, err := service.Case(ctx, oldCaseID)
if err != nil || oldDetail.Case.Status != domain.ModerationCaseResolved ||
oldDetail.Actions[0].Status != domain.ModerationActionSuperseded {
t.Fatalf("old detail=%+v err=%v", oldDetail, err)
}
newDetail, _, err := service.Case(ctx, newCaseID)
if err != nil || newDetail.Case.Status != domain.ModerationCaseResolved ||
newDetail.Actions[0].Status != domain.ModerationActionSucceeded {
t.Fatalf("new detail=%+v err=%v", newDetail, err)
}
appealTarget := domain.Peer{Type: domain.PeerTypeUser, ID: base + 10}
appealedCaseID, appealedActionID := createDecision(
appealTarget, base+11, "appealed", "pg-appealed", now.Add(10*time.Second),
)
actions, err := store.ClaimModerationActions(ctx, now.Add(13*time.Second), 10, time.Minute)
if err != nil || len(actions) != 1 || actions[0].ID != appealedActionID {
t.Fatalf("appealed action=%+v err=%v", actions, err)
}
if err := store.CompleteModerationAction(
ctx, actions[0].ID, actions[0].Attempts, true, "", time.Time{},
now.Add(14*time.Second),
); err != nil {
t.Fatal(err)
}
appeal, _, err := service.SubmitAppeal(
ctx, appealedCaseID, appealTarget.ID, "please review", now.Add(15*time.Second),
)
if err != nil {
t.Fatal(err)
}
_, _ = createDecision(
appealTarget, base+12, "newer", "pg-newer-owner", now.Add(16*time.Second),
)
appealedDetail, _, err := service.Case(ctx, appealedCaseID)
if err != nil {
t.Fatal(err)
}
appealClaim, err := service.ClaimCase(
ctx, appealedCaseID, appealedDetail.Case.Version, "pg-owner", now.Add(19*time.Second),
)
if err != nil {
t.Fatal(err)
}
_, _, err = service.ReviewAppeal(ctx, domain.ModerationDecisionRequest{
CaseID: appealedCaseID, AppealID: appeal.ID,
ExpectedVersion: appealClaim.Version, Actor: "pg-owner",
Reason: "grant", CommandID: "pg-stale-appeal",
Kind: domain.ModerationDecisionAppealGrant,
Actions: []domain.ModerationActionDraft{{
Kind: domain.ModerationActionClearPeerFlags, Payload: []byte(`{}`),
}},
CreatedAt: now.Add(20 * time.Second),
})
if !errors.Is(err, domain.ErrModerationActionConflict) {
t.Fatalf("ReviewAppeal error=%v", err)
}
}
func cleanupModerationReport(t *testing.T, pool *pgxpool.Pool, reportID int64) {
t.Helper()
ctx := context.Background()
tx, err := pool.Begin(ctx)
if err != nil {
t.Errorf("begin moderation cleanup: %v", err)
return
}
defer func() { _ = tx.Rollback(ctx) }()
var caseID int64
err = tx.QueryRow(ctx, `
SELECT case_id FROM moderation_case_reports WHERE report_id = $1`,
reportID,
).Scan(&caseID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
t.Errorf("find moderation cleanup case: %v", err)
return
}
if _, err := tx.Exec(ctx, "DELETE FROM sponsored_message_impressions WHERE report_id = $1", reportID); err != nil {
t.Errorf("cleanup sponsored impression: %v", err)
return
}
if _, err := tx.Exec(ctx, "DELETE FROM channel_antispam_decisions WHERE report_id = $1", reportID); err != nil {
t.Errorf("cleanup anti-spam decision: %v", err)
return
}
if caseID > 0 {
for _, statement := range []string{
"DELETE FROM moderation_actions WHERE case_id = $1",
"DELETE FROM moderation_decisions WHERE case_id = $1",
"DELETE FROM moderation_appeal_links WHERE case_id = $1",
"DELETE FROM moderation_appeals WHERE case_id = $1",
"DELETE FROM moderation_case_reports WHERE case_id = $1",
"DELETE FROM moderation_cases WHERE id = $1",
} {
if _, err := tx.Exec(ctx, statement, caseID); err != nil {
t.Errorf("moderation cleanup %q: %v", statement, err)
return
}
}
}
if _, err := tx.Exec(ctx, "DELETE FROM moderation_legacy_ephemeral_migrations WHERE moderation_report_id = $1", reportID); err != nil {
t.Errorf("cleanup legacy moderation mapping: %v", err)
return
}
if _, err := tx.Exec(ctx, "DELETE FROM moderation_reports WHERE id = $1", reportID); err != nil {
t.Errorf("cleanup moderation report: %v", err)
return
}
if err := tx.Commit(ctx); err != nil {
t.Errorf("commit moderation cleanup: %v", err)
}
}