feat(server): webhook 薄壳化——保留 X-Pangolin HMAC 协议,铸码走库单事务原语(#codes-lib)

This commit is contained in:
wangjia
2026-07-10 14:45:08 +08:00
parent 0822d22e2c
commit d1e2fed563
3 changed files with 78 additions and 76 deletions
@@ -3,9 +3,11 @@ package codes_test
import (
"context"
"database/sql"
"errors"
"testing"
"time"
libcodes "github.com/wangjia/codes"
"github.com/wangjia/pangolin/server/internal/codes"
)
@@ -113,6 +115,50 @@ func TestRedeemCreatesSubscription(t *testing.T) {
}
}
// TestWebhookMintsIntoLibTables verifies Store.MintOne (webhook 铸码核心):
// lands in the shared lib's new tables, entitlement is a duration payload,
// and a duplicate code_hash leaves no orphan batch row (the old
// two-statement webhook implementation leaked one batch per dupe delivery).
func TestWebhookMintsIntoLibTables(t *testing.T) {
ctx := context.Background()
db := openMigratedSQLite(t)
store := codes.NewStore(db)
plain, err := codes.GenerateCode()
if err != nil {
t.Fatal(err)
}
canonical := mustCanonical(t, plain)
ent, err := libcodesEnt("pro", 30)
if err != nil {
t.Fatal(err)
}
if err := store.MintOne(ctx, codes.Hash(canonical), ent, codes.ChannelStore, "webhook", "n1"); err != nil {
t.Fatalf("MintOne: %v", err)
}
var batches, ccount int
if err := db.QueryRow(`SELECT COUNT(*) FROM codes_batches`).Scan(&batches); err != nil || batches != 1 {
t.Fatalf("batches = %d (err=%v)", batches, err)
}
if err := db.QueryRow(`SELECT COUNT(*) FROM codes WHERE code_hash=?`, codes.Hash(canonical)).Scan(&ccount); err != nil || ccount != 1 {
t.Fatalf("codes = %d (err=%v)", ccount, err)
}
// 同 hash 再灌:ErrDuplicate 且 batch 数不变(无孤儿——旧实现的 I2 缺陷在此修复)。
err = store.MintOne(ctx, codes.Hash(canonical), ent, codes.ChannelStore, "webhook", "n2")
if !errors.Is(err, codes.ErrDuplicate) {
t.Fatalf("err = %v, want ErrDuplicate", err)
}
if err := db.QueryRow(`SELECT COUNT(*) FROM codes_batches`).Scan(&batches); err != nil || batches != 1 {
t.Fatalf("batches after dup = %d, want 1 (no orphan)", batches)
}
}
// libcodesEnt: 测试侧小工具,避免测试文件 import 库包名与宿主包名混淆。
func libcodesEnt(plan string, days int) (libcodes.Entitlement, error) {
return libcodes.NewDurationEntitlement(plan, days)
}
func mustCanonical(t *testing.T, code string) string {
t.Helper()
c, err := codes.Canonicalize(code)