feat: sync durable moderation and appeals
This commit is contained in:
parent
e1a95c7318
commit
9f467f4be7
140 changed files with 13730 additions and 316 deletions
178
internal/app/moderation/appeal_links_test.go
Normal file
178
internal/app/moderation/appeal_links_test.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package moderation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func TestAppealLinkSubmissionIsHashOnlyIdempotentAndExpires(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Unix(1_750_000_000, 0).UTC()
|
||||
store := memory.NewModerationReportStore()
|
||||
service := NewService(store)
|
||||
target := domain.Peer{Type: domain.PeerTypeUser, ID: 202}
|
||||
if _, _, err := service.AcceptReport(ctx, domain.ModerationReportDraft{
|
||||
ReporterUserID: 101, 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,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cases, err := service.ListCases(ctx, domain.ModerationCaseFilter{Limit: 10})
|
||||
if err != nil || len(cases) != 1 {
|
||||
t.Fatalf("cases=%+v err=%v", cases, err)
|
||||
}
|
||||
claimed, err := service.ClaimCase(
|
||||
ctx, cases[0].ID, cases[0].Version, "reviewer", now.Add(time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
detail, _, err := service.DecideCase(ctx, domain.ModerationDecisionRequest{
|
||||
CaseID: claimed.ID, ExpectedVersion: claimed.Version,
|
||||
Actor: "reviewer", Reason: "confirmed", CommandID: "appeal-link-decision",
|
||||
Kind: domain.ModerationDecisionViolation,
|
||||
Actions: []domain.ModerationActionDraft{{
|
||||
Kind: domain.ModerationActionMarkFake, Payload: []byte(`{}`),
|
||||
}},
|
||||
CreatedAt: now.Add(2 * time.Second),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token, err := service.IssueAppealLink(
|
||||
ctx, claimed.ID, target.ID, now.Add(24*time.Hour), now.Add(3*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(token) != 43 {
|
||||
t.Fatalf("token length=%d", len(token))
|
||||
}
|
||||
link, found, err := service.ResolveAppealLink(ctx, token, now.Add(4*time.Second))
|
||||
if err != nil || !found || link.TokenHash == ([32]byte{}) {
|
||||
t.Fatalf("link=%+v found=%v err=%v", link, found, err)
|
||||
}
|
||||
expiredToken, err := service.IssueAppealLink(
|
||||
ctx, claimed.ID, target.ID, now.Add(10*time.Second), now.Add(4*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < domain.MaxModerationAppealLinksPerCase-2; i++ {
|
||||
issuedAt := now.Add(time.Duration(20+i) * time.Second)
|
||||
if _, err := service.IssueAppealLink(
|
||||
ctx, claimed.ID, target.ID, now.Add(time.Hour), issuedAt,
|
||||
); err != nil {
|
||||
t.Fatalf("issue bounded link %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
if _, err := service.IssueAppealLink(
|
||||
ctx, claimed.ID, target.ID, now.Add(time.Hour), now.Add(time.Minute),
|
||||
); !errors.Is(err, domain.ErrModerationActionConflict) {
|
||||
t.Fatalf("appeal link overflow err=%v", err)
|
||||
}
|
||||
if actions, err := store.ClaimModerationActions(
|
||||
ctx, now.Add(5*time.Second), 10, time.Minute,
|
||||
); err != nil || len(actions) != 1 {
|
||||
t.Fatalf("actions=%+v err=%v", actions, err)
|
||||
} else if err := store.CompleteModerationAction(
|
||||
ctx, actions[0].ID, actions[0].Attempts, true, "",
|
||||
time.Time{}, now.Add(6*time.Second),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
appeal, created, err := service.SubmitAppealLink(
|
||||
ctx, token, "The account was impersonated.", now.Add(7*time.Second),
|
||||
)
|
||||
if err != nil || !created || appeal.CaseID != claimed.ID ||
|
||||
appeal.AppellantUserID != target.ID {
|
||||
t.Fatalf("appeal=%+v created=%v err=%v", appeal, created, err)
|
||||
}
|
||||
retry, created, err := service.SubmitAppealLink(
|
||||
ctx, token, "A different retry body must not create another appeal.",
|
||||
now.Add(8*time.Second),
|
||||
)
|
||||
if err != nil || created || retry.ID != appeal.ID || retry.Text != appeal.Text {
|
||||
t.Fatalf("retry=%+v created=%v err=%v", retry, created, err)
|
||||
}
|
||||
appealed, found, err := service.Case(ctx, detail.Case.ID)
|
||||
if err != nil || !found ||
|
||||
appealed.Case.Status != domain.ModerationCaseAppealReview ||
|
||||
len(appealed.Appeals) != 1 {
|
||||
t.Fatalf("appealed=%+v found=%v err=%v", appealed, found, err)
|
||||
}
|
||||
appealClaim, err := service.ClaimCase(
|
||||
ctx, appealed.Case.ID, appealed.Case.Version,
|
||||
"appeal-reviewer", now.Add(8*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grant := domain.ModerationDecisionRequest{
|
||||
CaseID: appealClaim.ID, AppealID: appeal.ID,
|
||||
ExpectedVersion: appealClaim.Version, Actor: "appeal-reviewer",
|
||||
Reason: "original evidence was insufficient",
|
||||
CommandID: "appeal-grant-without-remedy",
|
||||
Kind: domain.ModerationDecisionAppealGrant,
|
||||
CreatedAt: now.Add(9 * time.Second),
|
||||
}
|
||||
if _, _, err := service.ReviewAppeal(ctx, grant); !errors.Is(
|
||||
err, domain.ErrModerationActionInvalid,
|
||||
) {
|
||||
t.Fatalf("grant without required flag remedy err=%v", err)
|
||||
}
|
||||
grant.CommandID = "appeal-grant-with-remedy"
|
||||
grant.Actions = []domain.ModerationActionDraft{{
|
||||
Kind: domain.ModerationActionClearPeerFlags, Payload: []byte(`{}`),
|
||||
}}
|
||||
granted, created, err := service.ReviewAppeal(ctx, grant)
|
||||
if err != nil || !created ||
|
||||
granted.Case.Status != domain.ModerationCaseActionPending {
|
||||
t.Fatalf("granted=%+v created=%v err=%v", granted, created, err)
|
||||
}
|
||||
remedies, err := store.ClaimModerationActions(
|
||||
ctx, now.Add(10*time.Second), 10, time.Minute,
|
||||
)
|
||||
if err != nil || len(remedies) != 1 ||
|
||||
remedies[0].Kind != domain.ModerationActionClearPeerFlags {
|
||||
t.Fatalf("remedies=%+v err=%v", remedies, err)
|
||||
}
|
||||
if err := store.CompleteModerationAction(
|
||||
ctx, remedies[0].ID, remedies[0].Attempts, true, "",
|
||||
time.Time{}, now.Add(11*time.Second),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dismissed, found, err := service.Case(ctx, appealed.Case.ID)
|
||||
if err != nil || !found ||
|
||||
dismissed.Case.Status != domain.ModerationCaseDismissed {
|
||||
t.Fatalf("dismissed=%+v found=%v err=%v", dismissed, found, err)
|
||||
}
|
||||
|
||||
if _, found, err := service.ResolveAppealLink(
|
||||
ctx, expiredToken, now.Add(10*time.Second),
|
||||
); err != nil || found {
|
||||
t.Fatalf("expired resolve found=%v err=%v", found, err)
|
||||
}
|
||||
if _, _, err := service.SubmitAppealLink(
|
||||
ctx, expiredToken, "too late", now.Add(10*time.Second),
|
||||
); !errors.Is(err, domain.ErrModerationAppealLinkInvalid) {
|
||||
t.Fatalf("expired submit err=%v", err)
|
||||
}
|
||||
if _, _, err := service.ResolveAppealLink(ctx, "not-a-token", now); !errors.Is(
|
||||
err, domain.ErrModerationAppealLinkInvalid,
|
||||
) {
|
||||
t.Fatalf("invalid token err=%v", err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue