feat(server): Redeem 换芯 GuardedRedeem——grant 回调同事务复刻订阅叠加+双审计(#codes-lib)
This commit is contained in:
@@ -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) ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user