feat(server): Redeem 换芯 GuardedRedeem——grant 回调同事务复刻订阅叠加+双审计(#codes-lib)

This commit is contained in:
wangjia
2026-07-10 14:34:30 +08:00
parent e128c96d22
commit 0822d22e2c
5 changed files with 321 additions and 324 deletions
+8 -80
View File
@@ -6,7 +6,6 @@ import (
"testing"
"time"
"github.com/wangjia/pangolin/server/internal/codes"
"github.com/wangjia/pangolin/server/internal/config"
dbx "github.com/wangjia/pangolin/server/internal/db"
"github.com/wangjia/pangolin/server/internal/nodes"
@@ -190,85 +189,14 @@ func TestSQLite_PersistCredentialUpsert(t *testing.T) {
}
}
func TestSQLite_CodesRedeemFlow(t *testing.T) {
// SKIP (codes-lib-integration Task 1): migration 000020 renames codes/code_batches
// -> legacy_codes/legacy_code_batches to make room for the github.com/wangjia/codes
// shared library's own `codes` table. This test exercises internal/codes.Store's
// low-level methods (CreateBatch/CreateCode/FindCodeByHashForUpdate/MarkRedeemed),
// which still target the old table names and are slated for removal once
// internal/codes/store.go is re-pointed at the library (Task 4) and the redeem
// path moves to codes.GuardedRedeem (Task 5, which deletes this test in favor of
// an equivalent Service-level sqlite case — see plan §Task 5).
t.Skip("superseded by codes-lib-integration Task 5 (internal/codes Store/Redeem move to shared lib); low-level methods here still target renamed legacy_* tables")
ctx := context.Background()
db := openSQLite(t)
seedUser(t, db, 1)
cs := codes.NewStore(db)
// Seed a batch + an 'unused' code for plan 'pro' (seeded id resolved via store).
planID, err := cs.GetPlanID(ctx, codes.PlanPro)
if err != nil {
t.Fatalf("plan id: %v", err)
}
batchID, err := cs.CreateBatch(ctx, codes.ChannelManual, "tester", "")
if err != nil {
t.Fatalf("create batch: %v", err)
}
const hash = "abc123hash"
if err := cs.CreateCode(ctx, hash, planID, 30, batchID); err != nil {
t.Fatalf("create code: %v", err)
}
// Redeem inside a tx: lock the code (FOR UPDATE on mysql / BEGIN IMMEDIATE on
// sqlite), mark redeemed, create a subscription with Go-computed expiry.
tx, err := cs.BeginTx(ctx)
if err != nil {
t.Fatalf("begin: %v", err)
}
cr, err := cs.FindCodeByHashForUpdate(ctx, tx, hash)
if err != nil || cr == nil {
_ = tx.Rollback()
t.Fatalf("find for update: cr=%v err=%v", cr, err)
}
if cr.Status != "unused" {
_ = tx.Rollback()
t.Fatalf("code status = %q, want unused", cr.Status)
}
if err := cs.MarkRedeemed(ctx, tx, cr.ID, 1); err != nil {
_ = tx.Rollback()
t.Fatalf("mark redeemed: %v", err)
}
subID, err := cs.CreateSubscription(ctx, tx, 1, planID, 30, time.Time{})
if err != nil {
_ = tx.Rollback()
t.Fatalf("create sub: %v", err)
}
// Extend by 10 more days (Go-side max(expires,now)+days).
if err := cs.ExtendSubscription(ctx, tx, subID, 10); err != nil {
_ = tx.Rollback()
t.Fatalf("extend: %v", err)
}
if err := tx.Commit(); err != nil {
t.Fatalf("commit: %v", err)
}
// Verify: code redeemed; subscription expires ~40 days out.
var status string
if err := db.QueryRow(`SELECT status FROM codes WHERE id=?`, cr.ID).Scan(&status); err != nil {
t.Fatalf("read code: %v", err)
}
if status != "redeemed" {
t.Errorf("code status = %q, want redeemed", status)
}
var expires time.Time
if err := db.QueryRow(`SELECT expires_at FROM subscriptions WHERE id=?`, subID).Scan(&expires); err != nil {
t.Fatalf("read sub: %v", err)
}
wantMin := time.Now().UTC().AddDate(0, 0, 39)
if expires.Before(wantMin) {
t.Errorf("subscription expires_at = %v, want ≥ ~40 days out (%v)", expires, wantMin)
}
}
// NOTE: TestSQLite_CodesRedeemFlow (formerly here, low-level
// internal/codes.Store methods CreateBatch/CreateCode/FindCodeByHashForUpdate/
// MarkRedeemed) was removed in codes-lib-integration Task 5: the redeem path
// now goes through the shared github.com/wangjia/codes library
// (codes.GuardedRedeem), and those low-level methods no longer exist on
// internal/codes.Store. Equivalent coverage — Service-level, against the real
// call path — lives in internal/codes/service_sqlite_test.go
// (TestRedeem*).
// --- seed helpers (raw SQL, satisfy foreign keys) ---