0ae7769abe
- gateway: TestSettleConcurrentSameOrder 真并发钉住 MarkAttemptPaid 行锁不变量 (文件型 sqlite,规避 in-memory cache=shared 的 SQLITE_LOCKED_SHAREDCACHE)。 - gateway: e2e_fullchain_test.go 补下单→回调→settle→webhook 实际 HTTP 投递→ delivered 整链(httptest server + 真实 store.WebhookStore/webhook.Notifier)。 - reconcile: main.go 装配抽到 reconcile.Assemble(+Runner.TaskNames 访问器), 补 assembly_test.go 钉住 7 个后台任务全部注册 + crypto 缺渠道/interval=0 分支。 - alipay: 补验签健壮性(未知多余字段/字段乱序/中文unicode/空值字段),意外定位 vendor Encoder 对空值字段的真实语义与官方文档描述不同,已记录在测试注释。 - money: 补 TestZeroDecimalCurrencyJPYNotSupported,记录 JPY/KRW 当前未注册进 exponents(接入会先踩 ErrUnknownCurrency,不是乘除法坑)。
154 lines
6.4 KiB
Go
154 lines
6.4 KiB
Go
package reconcile_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/wangjia/pay/config"
|
|
"github.com/wangjia/pay/internal/accounts"
|
|
"github.com/wangjia/pay/internal/gateway"
|
|
"github.com/wangjia/pay/internal/model"
|
|
"github.com/wangjia/pay/internal/provider"
|
|
"github.com/wangjia/pay/internal/provider/crypto"
|
|
"github.com/wangjia/pay/internal/provider/fake"
|
|
"github.com/wangjia/pay/internal/reconcile"
|
|
"github.com/wangjia/pay/internal/store"
|
|
)
|
|
|
|
// TestAssembleRegistersAllExpectedTasks 钉住 main.go 里那一长串 reconcile 任务(P6
|
|
// order-expire/usage-refresh/refund-apply-sweep/refund-stuck-alert/sync-pending/
|
|
// paid-spotcheck + P8 Task6 crypto-orphan-scan)确实全部被注册 —— main() 原先把这段
|
|
// 装配写死在函数体内,没有任何测试保证"漏挂一个任务"能被发现;抽成
|
|
// reconcile.Assemble(见 internal/reconcile/assembly.go)后可以直接断言注册出的任务名
|
|
// 集合,main.go 现在只是薄薄一层调用(逐行照抄原实现,未改调度语义)。
|
|
//
|
|
// pReg 同时注册 fake + crypto 两个渠道:crypto-orphan-scan 只在 crypto 渠道已注册时
|
|
// 才由 AddCryptoJobs 挂载(见其函数注释),不带 crypto 就测不到这第 7 个任务。
|
|
func TestAssembleRegistersAllExpectedTasks(t *testing.T) {
|
|
db := model.OpenTestDB(t)
|
|
orders := store.NewOrderStore(db)
|
|
refunds := store.NewRefundStore(db)
|
|
subs := store.NewSubscriptionStore(db)
|
|
chargebacks := store.NewChargebackStore(db)
|
|
orphans := store.NewOrphanStore(db)
|
|
|
|
preg := provider.NewRegistry()
|
|
fp := fake.New()
|
|
preg.Register(fp)
|
|
acctReg := accounts.New([]config.AccountConfig{
|
|
{AccountID: "fake-a1", Channel: "fake", Region: "global", Enabled: true},
|
|
{AccountID: "crypto-a1", Channel: "crypto", Region: "global", Enabled: true, CredentialEnvPrefix: "assembletest"},
|
|
})
|
|
preg.Register(crypto.New(acctReg))
|
|
picker := accounts.NewRouter(acctReg, nil, nil)
|
|
|
|
usage := reconcile.NewUsageSource(orders, nil)
|
|
gw := gateway.New(orders, refunds, preg, picker, stubResolver{}, nopEnq{}, "global", subs, chargebacks)
|
|
|
|
rc := config.ReconcileConfig{
|
|
Enabled: true, OrderTTLMin: 30, ExpireEverySec: 60, SyncEverySec: 60, UsageEverySec: 60,
|
|
SpotCheckEverySec: 60, SpotCheckWindowMin: 60, OrphanEverySec: 60, OrphanWindowMin: 60,
|
|
RefundApplyEverySec: 60, RefundStuckWarnMin: 60, RefundApplyLookbackMin: 60,
|
|
}
|
|
runner := reconcile.Assemble(orders, refunds, usage, gw, preg, acctReg, orphans, rc)
|
|
|
|
want := []string{
|
|
"order-expire", "usage-refresh", "refund-apply-sweep", "refund-stuck-alert",
|
|
"sync-pending", "paid-spotcheck", "crypto-orphan-scan",
|
|
}
|
|
got := runner.TaskNames()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("task names = %v (%d), want %d: %v", got, len(got), len(want), want)
|
|
}
|
|
seen := make(map[string]bool, len(got))
|
|
for _, n := range got {
|
|
seen[n] = true
|
|
}
|
|
for _, w := range want {
|
|
if !seen[w] {
|
|
t.Fatalf("缺任务 %q,got %v", w, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAssembleSkipsCryptoOrphanScanWithoutCryptoChannel crypto 渠道未启用时(providerbuild
|
|
// 按账户配置决定是否注册,见 internal/providerbuild),AddCryptoJobs 安全跳过——不应
|
|
// 注册 crypto-orphan-scan,其余 6 个本地/网络任务照常注册,总数少一个。
|
|
func TestAssembleSkipsCryptoOrphanScanWithoutCryptoChannel(t *testing.T) {
|
|
db := model.OpenTestDB(t)
|
|
orders := store.NewOrderStore(db)
|
|
refunds := store.NewRefundStore(db)
|
|
subs := store.NewSubscriptionStore(db)
|
|
chargebacks := store.NewChargebackStore(db)
|
|
orphans := store.NewOrphanStore(db)
|
|
|
|
preg := provider.NewRegistry() // 只 fake,没有 crypto
|
|
fp := fake.New()
|
|
preg.Register(fp)
|
|
acctReg := accounts.New([]config.AccountConfig{
|
|
{AccountID: "fake-a1", Channel: "fake", Region: "global", Enabled: true},
|
|
})
|
|
picker := accounts.NewRouter(acctReg, nil, nil)
|
|
|
|
usage := reconcile.NewUsageSource(orders, nil)
|
|
gw := gateway.New(orders, refunds, preg, picker, stubResolver{}, nopEnq{}, "global", subs, chargebacks)
|
|
|
|
rc := config.ReconcileConfig{
|
|
Enabled: true, OrderTTLMin: 30, ExpireEverySec: 60, SyncEverySec: 60, UsageEverySec: 60,
|
|
SpotCheckEverySec: 60, SpotCheckWindowMin: 60, OrphanEverySec: 60, OrphanWindowMin: 60,
|
|
RefundApplyEverySec: 60, RefundStuckWarnMin: 60, RefundApplyLookbackMin: 60,
|
|
}
|
|
runner := reconcile.Assemble(orders, refunds, usage, gw, preg, acctReg, orphans, rc)
|
|
|
|
got := runner.TaskNames()
|
|
if len(got) != 6 {
|
|
t.Fatalf("无 crypto 渠道时应只注册 6 个任务(缺 crypto-orphan-scan), got %v", got)
|
|
}
|
|
for _, n := range got {
|
|
if n == "crypto-orphan-scan" {
|
|
t.Fatalf("未启用 crypto 渠道时不应注册 crypto-orphan-scan, got %v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAssembleZeroIntervalSkipsThatTaskOnly 覆盖 Runner.add 的 interval<=0 守卫在装配层
|
|
// 的联动:某一项周期配成 0(operator 配置疏漏,如 usage_every_sec: 0)只会让那一个任务
|
|
// 不注册,其余任务不受影响(呼应 runner_test.go 的
|
|
// TestRunnerZeroIntervalSkipsRegistrationAndStartDoesNotPanic,但那里是直接摆弄 Runner,
|
|
// 这里钉住 Assemble 真按 config 字段逐个转发 interval,没有哪个任务共用错了字段)。
|
|
func TestAssembleZeroIntervalSkipsThatTaskOnly(t *testing.T) {
|
|
db := model.OpenTestDB(t)
|
|
orders := store.NewOrderStore(db)
|
|
refunds := store.NewRefundStore(db)
|
|
subs := store.NewSubscriptionStore(db)
|
|
chargebacks := store.NewChargebackStore(db)
|
|
orphans := store.NewOrphanStore(db)
|
|
|
|
preg := provider.NewRegistry()
|
|
fp := fake.New()
|
|
preg.Register(fp)
|
|
acctReg := accounts.New([]config.AccountConfig{{AccountID: "fake-a1", Channel: "fake", Region: "global", Enabled: true}})
|
|
picker := accounts.NewRouter(acctReg, nil, nil)
|
|
|
|
usage := reconcile.NewUsageSource(orders, nil)
|
|
gw := gateway.New(orders, refunds, preg, picker, stubResolver{}, nopEnq{}, "global", subs, chargebacks)
|
|
|
|
rc := config.ReconcileConfig{
|
|
Enabled: true, OrderTTLMin: 30, ExpireEverySec: 60, SyncEverySec: 60,
|
|
UsageEverySec: 0, // 疏漏:只这一项配成 0
|
|
SpotCheckEverySec: 60, SpotCheckWindowMin: 60, OrphanEverySec: 60, OrphanWindowMin: 60,
|
|
RefundApplyEverySec: 60, RefundStuckWarnMin: 60, RefundApplyLookbackMin: 60,
|
|
}
|
|
runner := reconcile.Assemble(orders, refunds, usage, gw, preg, acctReg, orphans, rc)
|
|
|
|
got := runner.TaskNames()
|
|
for _, n := range got {
|
|
if n == "usage-refresh" {
|
|
t.Fatalf("usage_every_sec=0 时 usage-refresh 不应注册, got %v", got)
|
|
}
|
|
}
|
|
// 其余 5 个(order-expire/refund-apply-sweep/refund-stuck-alert/sync-pending/paid-spotcheck)照常。
|
|
if len(got) != 5 {
|
|
t.Fatalf("只 usage-refresh 应被跳过,其余应全注册, got %v", got)
|
|
}
|
|
}
|