Files
wangjia e128c96d22 feat(server): CreateBatch/生成器切换到 codes 库(Mint 单事务全成或全无)(#codes-lib)
Store 挂库 store(NewStore 内部构造 libcodes.Store,签名不变);
generator.go 三函数委托 libcodes,ErrDuplicate 别名同一哨兵;
Service.CreateBatch 走 libcodes.Mint(签名不变)。openMigratedSQLite
挪到共享 sqlite_helper_test.go,backfill_test.go/service_sqlite_test.go
共用。Store.CreateBatch/CreateCode/CodeExistsByHash 原样保留供
webhook.go(Task 6 改用新原语,Task 7 删除)。
2026-07-10 14:24:18 +08:00

115 lines
3.7 KiB
Go

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)
}
}