fix(admin): sync moderation review state refresh

This commit is contained in:
iamxvbaba 2026-07-27 01:01:30 +08:00
parent e75fd04a28
commit 90649b5b67
4 changed files with 255 additions and 9 deletions

View file

@ -727,7 +727,9 @@ func (s *Server) handleModerationCases(w http.ResponseWriter, r *http.Request) {
writeModerationError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"cases": items})
writeJSON(w, http.StatusOK, map[string]any{
"cases": moderationCasesResponse(items),
})
}
func (s *Server) handleModerationCase(w http.ResponseWriter, r *http.Request) {
@ -744,7 +746,7 @@ func (s *Server) handleModerationCase(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotFound, "moderation case not found")
return
}
writeJSON(w, http.StatusOK, detail)
writeJSON(w, http.StatusOK, moderationCaseDetailResponse(detail))
}
func (s *Server) handleModerationReport(w http.ResponseWriter, r *http.Request) {
@ -761,7 +763,7 @@ func (s *Server) handleModerationReport(w http.ResponseWriter, r *http.Request)
writeError(w, http.StatusNotFound, "moderation report not found")
return
}
writeJSON(w, http.StatusOK, report)
writeJSON(w, http.StatusOK, moderationReportResponse(report))
}
func (s *Server) handleClaimModerationCase(w http.ResponseWriter, r *http.Request) {
@ -800,7 +802,7 @@ func (s *Server) handleDecideModerationCase(w http.ResponseWriter, r *http.Reque
return
}
writeJSON(w, http.StatusOK, map[string]any{
"created": created, "case": detail,
"created": created, "case": moderationCaseDetailResponse(detail),
})
}
@ -855,10 +857,37 @@ func (s *Server) handleReviewModerationAppeal(w http.ResponseWriter, r *http.Req
return
}
writeJSON(w, http.StatusOK, map[string]any{
"created": created, "case": detail,
"created": created, "case": moderationCaseDetailResponse(detail),
})
}
func moderationCasesResponse(items []domain.ModerationCase) []domain.ModerationCase {
if items == nil {
return []domain.ModerationCase{}
}
return items
}
func moderationCaseDetailResponse(detail domain.ModerationCaseDetail) domain.ModerationCaseDetail {
if detail.Decisions == nil {
detail.Decisions = []domain.ModerationDecision{}
}
if detail.Actions == nil {
detail.Actions = []domain.ModerationAction{}
}
if detail.Appeals == nil {
detail.Appeals = []domain.ModerationAppeal{}
}
return detail
}
func moderationReportResponse(report domain.ModerationReport) domain.ModerationReport {
if report.MediaHolds == nil {
report.MediaHolds = []domain.ModerationMediaHold{}
}
return report
}
func moderationDecisionDomain(caseID, appealID int64, request moderationDecisionRequest) domain.ModerationDecisionRequest {
actions := make([]domain.ModerationActionDraft, 0, len(request.Actions))
for _, action := range request.Actions {

View file

@ -3,6 +3,7 @@ package adminapi
import (
"bytes"
"context"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
@ -121,6 +122,147 @@ func TestAdminAPIModerationQueueDecisionAndAppealReview(t *testing.T) {
}
}
type emptyModerationCollectionsService struct {
fakeService
}
func (emptyModerationCollectionsService) ModerationCases(
context.Context,
domain.ModerationCaseFilter,
) ([]domain.ModerationCase, error) {
return nil, nil
}
func (emptyModerationCollectionsService) ModerationCase(
_ context.Context,
caseID int64,
) (domain.ModerationCaseDetail, bool, error) {
return domain.ModerationCaseDetail{
Case: domain.ModerationCase{ID: caseID},
ReportIDs: []int64{9},
}, true, nil
}
func (emptyModerationCollectionsService) ModerationReport(
_ context.Context,
reportID int64,
) (domain.ModerationReport, bool, error) {
return domain.ModerationReport{
ID: reportID,
Items: []domain.ModerationReportItem{{ItemID: 10}},
}, true, nil
}
func (emptyModerationCollectionsService) DecideModerationCase(
_ context.Context,
request domain.ModerationDecisionRequest,
) (domain.ModerationCaseDetail, bool, error) {
return domain.ModerationCaseDetail{
Case: domain.ModerationCase{ID: request.CaseID},
ReportIDs: []int64{9},
}, true, nil
}
func (emptyModerationCollectionsService) ReviewModerationAppeal(
_ context.Context,
request domain.ModerationDecisionRequest,
) (domain.ModerationCaseDetail, bool, error) {
return domain.ModerationCaseDetail{
Case: domain.ModerationCase{ID: request.CaseID},
ReportIDs: []int64{9},
}, true, nil
}
func TestAdminAPIModerationCollectionsAreJSONArrays(t *testing.T) {
srv := &Server{token: "secret", svc: emptyModerationCollectionsService{}}
tests := []struct {
name string
method string
path string
body string
keys []string
nonEmptyKeys []string
nested string
}{
{
name: "empty queue", method: http.MethodGet,
path: "/v1/moderation/cases", keys: []string{"cases"},
},
{
name: "fresh case", method: http.MethodGet,
path: "/v1/moderation/cases/7",
keys: []string{"Decisions", "Actions", "Appeals"},
nonEmptyKeys: []string{"ReportIDs"},
},
{
name: "report without media holds", method: http.MethodGet,
path: "/v1/moderation/reports/9",
keys: []string{"MediaHolds"},
nonEmptyKeys: []string{"Items"},
},
{
name: "decision response", method: http.MethodPost,
path: "/v1/moderation/cases/7/decide", body: `{}`,
nested: "case",
keys: []string{"Decisions", "Actions", "Appeals"},
nonEmptyKeys: []string{"ReportIDs"},
},
{
name: "appeal review response", method: http.MethodPost,
path: "/v1/moderation/cases/7/appeals/8/review", body: `{}`,
nested: "case",
keys: []string{"Decisions", "Actions", "Appeals"},
nonEmptyKeys: []string{"ReportIDs"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.body))
req.Header.Set("Authorization", "Bearer secret")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
var response map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if tt.nested != "" {
nestedValue := response[tt.nested]
var ok bool
response, ok = nestedValue.(map[string]any)
if !ok {
t.Fatalf("%s=%T, want object; body=%s",
tt.nested, nestedValue, rec.Body.String())
}
}
for _, key := range tt.keys {
value, ok := response[key]
if !ok {
t.Fatalf("%s missing; body=%s", key, rec.Body.String())
}
items, ok := value.([]any)
if !ok || len(items) != 0 {
t.Fatalf("%s=%#v, want empty JSON array; body=%s",
key, value, rec.Body.String())
}
}
for _, key := range tt.nonEmptyKeys {
value, ok := response[key]
if !ok {
t.Fatalf("%s missing; body=%s", key, rec.Body.String())
}
items, ok := value.([]any)
if !ok || len(items) == 0 {
t.Fatalf("%s=%#v, want non-empty JSON array; body=%s",
key, value, rec.Body.String())
}
}
})
}
}
func TestAdminAPISetVerified(t *testing.T) {
srv := &Server{token: "secret", svc: fakeService{}}
req := httptest.NewRequest(http.MethodPost, "/v1/accounts/set-verified", strings.NewReader(`{"command_id":"c2","actor":"ops","reason":"official","dry_run":true,"user_id":1001,"verified":true}`))