// admin_claims_test.go — 单元测试 Stage 2 claim 状态机的纯函数校验。 // 数据库集成留给 stage2 QA curl 脚本;单测只覆盖纯逻辑分支: // - validateMarkPaidByType 的三个奖品类型 x 凭证字段组合 // - bucketByType 的类型 → 状态桶映射 // - claimToAdminView 的 nullable 字段渲染 package lottery import ( "testing" "time" modelLottery "github.com/perfect-panel/server/internal/model/lottery" "github.com/perfect-panel/server/internal/types" ) func TestValidateMarkPaidByType(t *testing.T) { cases := []struct { name string prizeType string txHash string deliveryRef string wantErr bool }{ {"crypto with tx_hash", modelLottery.PrizeTypeCrypto, "0xdeadbeef", "", false}, {"crypto missing tx_hash", modelLottery.PrizeTypeCrypto, "", "", true}, {"crypto ignores delivery_ref alone", modelLottery.PrizeTypeCrypto, "", "SF123", true}, {"physical with delivery_ref", modelLottery.PrizeTypePhysical, "", "SF123456", false}, {"physical missing delivery_ref", modelLottery.PrizeTypePhysical, "", "", true}, {"manual_other with tx_hash", modelLottery.PrizeTypeManualOther, "0xabc", "", false}, {"manual_other with delivery_ref", modelLottery.PrizeTypeManualOther, "", "SF00", false}, {"manual_other with both", modelLottery.PrizeTypeManualOther, "0xabc", "SF00", false}, {"manual_other with none", modelLottery.PrizeTypeManualOther, "", "", true}, {"unknown type", "auto_hallucinated", "", "", true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := validateMarkPaidByType(tc.prizeType, tc.txHash, tc.deliveryRef) if tc.wantErr && err == nil { t.Fatal("expected error, got nil") } if !tc.wantErr && err != nil { t.Fatalf("expected nil, got %v", err) } }) } } func TestBucketByType(t *testing.T) { s := &types.AdminLotteryClaimsSummary{} if bucketByType(s, modelLottery.PrizeTypeCrypto) != &s.Crypto { t.Fatal("crypto bucket mismatch") } if bucketByType(s, modelLottery.PrizeTypePhysical) != &s.Physical { t.Fatal("physical bucket mismatch") } if bucketByType(s, modelLottery.PrizeTypeManualOther) != &s.ManualOther { t.Fatal("manual_other bucket mismatch") } if bucketByType(s, "unknown") != nil { t.Fatal("unknown type must return nil") } } func TestClaimToAdminView_RendersNullableFields(t *testing.T) { submittedAt := time.Date(2026, 7, 1, 10, 0, 0, 0, time.UTC) paidAt := time.Date(2026, 7, 3, 14, 0, 0, 0, time.UTC) claim := modelLottery.Claim{ Id: 42, DrawId: 1234, UserId: 88, ActivityId: 100, PrizeType: modelLottery.PrizeTypeCrypto, Status: modelLottery.ClaimStatusPaid, ClaimData: `{"network":"BTC","address":"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"}`, SubmittedAt: &submittedAt, ExpiresAt: submittedAt.Add(24 * time.Hour), TxHash: "0xabcdef", PaidAt: &paidAt, } snap := modelLottery.PrizeSnapshot{ Type: modelLottery.PrizeTypeCrypto, Name: "1 BTC", Config: `{"amount":"1","currency":"BTC","networks":["BTC"]}`, } view := claimToAdminView(claim, snap, "user@example.com") if view.Id != 42 || view.DrawId != 1234 || view.User.Id != 88 { t.Fatalf("view IDs wrong: %+v", view) } if view.User.Email != "user@example.com" { t.Fatalf("Email = %q", view.User.Email) } if view.Status != modelLottery.ClaimStatusPaid { t.Fatalf("Status = %q", view.Status) } if view.SubmittedAt != submittedAt.Unix() { t.Fatalf("SubmittedAt = %d, want %d", view.SubmittedAt, submittedAt.Unix()) } if view.PaidAt != paidAt.Unix() { t.Fatalf("PaidAt = %d, want %d", view.PaidAt, paidAt.Unix()) } if view.Prize.Type != modelLottery.PrizeTypeCrypto { t.Fatalf("Prize.Type = %q", view.Prize.Type) } if len(view.ClaimData) == 0 { t.Fatal("ClaimData must be included when non-empty") } } func TestClaimToAdminView_NoSnapshotFallsBackToClaimPrizeType(t *testing.T) { claim := modelLottery.Claim{ Id: 1, DrawId: 2, PrizeType: modelLottery.PrizeTypePhysical, Status: modelLottery.ClaimStatusPendingClaim, ExpiresAt: time.Now().Add(time.Hour), } view := claimToAdminView(claim, modelLottery.PrizeSnapshot{}, "") if view.Prize.Type != modelLottery.PrizeTypePhysical { t.Fatalf("Prize.Type fallback = %q", view.Prize.Type) } }