package codes_test import ( "context" "testing" "time" "github.com/wangjia/pangolin/server/internal/codes" "github.com/wangjia/pangolin/server/internal/config" "github.com/wangjia/pangolin/server/internal/store" ) func TestBackfillLegacy(t *testing.T) { ctx := context.Background() db := openMigratedSQLite(t) // 造 legacy 数据(000020 之后 legacy 表空壳存在,直插即可)。plans 已 seed(000007)。 var proID int64 if err := db.QueryRow(`SELECT id FROM plans WHERE code='pro'`).Scan(&proID); err != nil { t.Fatalf("plan: %v", err) } batchAt := time.Date(2026, 5, 1, 8, 0, 0, 0, time.UTC) if _, err := db.Exec( `INSERT INTO legacy_code_batches (id, channel, created_by, note, created_at) VALUES (7, 'manual', 'admin:1', 'may batch', ?)`, batchAt); err != nil { t.Fatalf("seed batch: %v", err) } redeemedAt := time.Date(2026, 6, 2, 9, 30, 0, 0, time.UTC) seed := []struct { id int64 hash string status string redeemedBy any redeemedAt any }{ {101, "hash-unused-000000000000000000000000000000000000000000000000000001", "unused", nil, nil}, {102, "hash-redeemed-0000000000000000000000000000000000000000000000000002", "redeemed", int64(42), redeemedAt}, {103, "hash-void-00000000000000000000000000000000000000000000000000000003", "void", nil, nil}, } for _, c := range seed { if _, err := db.Exec( `INSERT INTO legacy_codes (id, code_hash, plan_id, duration_days, batch_id, status, redeemed_by, redeemed_at) VALUES (?, ?, ?, 30, 7, ?, ?, ?)`, c.id, c.hash, proID, c.status, c.redeemedBy, c.redeemedAt); err != nil { t.Fatalf("seed code %d: %v", c.id, err) } } n, err := codes.BackfillLegacy(ctx, db) if err != nil { t.Fatalf("BackfillLegacy: %v", err) } if n != 3 { t.Fatalf("migrated = %d, want 3", n) } // batch:id 保留 + 权益合成自第一条 code。 var kind, payload string if err := db.QueryRow( `SELECT entitlement_kind, entitlement_payload FROM codes_batches WHERE id=7`).Scan(&kind, &payload); err != nil { t.Fatalf("batch row: %v", err) } if kind != "duration" { t.Errorf("batch kind = %q", kind) } if payload != `{"plan":"pro","days":30}` { t.Errorf("batch payload = %s", payload) } // code:redeemed_by 映射 + created_at 取 batch 时间 + status passthrough。 var status, rby, cpayload string var cat time.Time if err := db.QueryRow( `SELECT status, redeemed_by, entitlement_payload, created_at FROM codes WHERE code_hash=?`, seed[1].hash).Scan(&status, &rby, &cpayload, &cat); err != nil { t.Fatalf("code row: %v", err) } if status != "redeemed" || rby != "user:42" { t.Errorf("status=%q redeemed_by=%q", status, rby) } if cpayload != `{"plan":"pro","days":30}` { t.Errorf("code payload = %s", cpayload) } if !cat.Equal(batchAt) { t.Errorf("created_at = %v, want batch created_at %v", cat, batchAt) } // 幂等:重复跑迁 0 行、不重不错。 n2, err := codes.BackfillLegacy(ctx, db) if err != nil || n2 != 0 { t.Fatalf("re-run: n=%d err=%v, want 0,nil", n2, err) } var cnt int if err := db.QueryRow(`SELECT COUNT(*) FROM codes`).Scan(&cnt); err != nil || cnt != 3 { t.Fatalf("codes count = %d (err=%v), want 3", cnt, err) } } func TestBackfillLegacyNoLegacyTables(t *testing.T) { // 全新库(未来 legacy 表 retire 后)也不报错。用一个没跑 pangolin 迁移、只有库表的 DB 模拟。 ctx := context.Background() db, err := store.Open(&config.Config{Driver: "sqlite", DSN: ":memory:"}) if err != nil { t.Fatalf("open: %v", err) } defer db.Close() if err := store.ApplyCodesLibMigrations(ctx, db, "sqlite"); err != nil { t.Fatalf("lib migrate: %v", err) } n, err := codes.BackfillLegacy(ctx, db) if err != nil || n != 0 { t.Fatalf("n=%d err=%v, want 0,nil", n, err) } }