e128c96d22
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 删除)。
115 lines
3.7 KiB
Go
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)
|
|
}
|
|
}
|