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) } }