Files
sundynix-agentix/sundynix-gateway/internal/payment/manager_test.go
T
Blizzard 3a4e1d53a5 refactor(payment): 渠道抽象成 Channel 接口 —— 接新渠道=加 adapter 不动骨架
PAYMENT_DESIGN §3 承诺的 internal/payment/channel.go 适配器接口此前不存在,微信硬编码在
manager/handler 里。补齐抽象:

- channel.go:Channel 接口(Name/CreatePay/QueryOrder/VerifyCallback)+ 统一 PayIntent/PayResult
  + 渠道名常量。入参用基本类型不吃 *store.PaymentOrder,payment 包不反依赖 store。
- Wechat 实现 Channel(编译期断言 var _ Channel);QueryResult 归一为 PayResult;CreatePay 返回 PayIntent。
- Manager 从「持一个 *Wechat」改为渠道注册表:Get(name)/Available()/Status(name)/ReloadWechat;
  按渠道名持有已装配实例,热重载不变。
- 回调路由收敛 /billing/callback/wechat → /billing/callback/:channel 按名路由(旧 notify URL 仍匹配);
  查单/掉单补偿据 order.Channel 路由,不再写死微信。下单支持可选 channel(缺省 wechat)。
- 支付宝/Stripe 现在真·只差一个 adapter+注册。唯一未泛化:回调 ack 应答格式(现微信态,注释标明)。
- payment 包首个测试:Manager 注册表 4 用例(空/注册摘除/空配置/配置不全)。build/vet/test/lint 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:56:24 +08:00

97 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package payment
import (
"context"
"net/http"
"testing"
)
// stubChannel 是测试用的假渠道,验证 Manager 注册表逻辑(不打真渠道网络)。
type stubChannel struct{ name string }
func (s stubChannel) Name() string { return s.name }
func (s stubChannel) CreatePay(_ context.Context, _, _ string, _ int64) (PayIntent, error) {
return PayIntent{CodeURL: "stub://" + s.name}, nil
}
func (s stubChannel) QueryOrder(_ context.Context, _ string) (PayResult, error) {
return PayResult{}, nil
}
func (s stubChannel) VerifyCallback(_ *http.Request) (PayResult, error) {
return PayResult{}, nil
}
// 空 ManagerGet 任意渠道为 nilAvailable 为空,Status 报未配置。
func TestManager_EmptyRegistry(t *testing.T) {
m := NewManager()
if m.Get(ChannelWechat) != nil {
t.Error("未注册渠道 Get 应为 nil")
}
if len(m.Available()) != 0 {
t.Errorf("空注册表 Available 应为空,得 %v", m.Available())
}
if ok, reason := m.Status(ChannelWechat); ok || reason == "" {
t.Errorf("空注册表 wechat 应未启用且有原因,得 ok=%v reason=%q", ok, reason)
}
}
// register 生效:注册后 Get 取到、Available 含之、Status 启用;置 nil 则摘除并留原因。
func TestManager_RegisterAndRemove(t *testing.T) {
m := NewManager()
m.register(ChannelWechat, stubChannel{name: ChannelWechat}, "")
if got := m.Get(ChannelWechat); got == nil || got.Name() != ChannelWechat {
t.Fatalf("注册后应能 Get 到 wechat,得 %v", got)
}
if ok, _ := m.Status(ChannelWechat); !ok {
t.Error("注册后 Status 应启用")
}
avail := m.Available()
if len(avail) != 1 || avail[0] != ChannelWechat {
t.Errorf("Available 应含 wechat,得 %v", avail)
}
// 再注册第二个渠道 → Available 有序返回两个。
m.register(ChannelAlipay, stubChannel{name: ChannelAlipay}, "")
if avail := m.Available(); len(avail) != 2 || avail[0] != ChannelAlipay || avail[1] != ChannelWechat {
t.Errorf("Available 应有序含 alipay,wechat,得 %v", avail)
}
// 置 nil(配置变坏/清空)→ 摘除,Get 为 nil、Status 未启用且保留原因。
m.register(ChannelWechat, nil, "配置不全,缺: mchid")
if m.Get(ChannelWechat) != nil {
t.Error("摘除后 Get 应为 nil")
}
if ok, reason := m.Status(ChannelWechat); ok || reason != "配置不全,缺: mchid" {
t.Errorf("摘除后应未启用且保留原因,得 ok=%v reason=%q", ok, reason)
}
}
// ReloadWechat 空配置:不算错、渠道保持隐藏、原因为「未配置」。
func TestManager_ReloadWechat_Empty(t *testing.T) {
m := NewManager()
if err := m.ReloadWechat(context.Background(), Config{}); err != nil {
t.Errorf("空配置 ReloadWechat 不应报错,得 %v", err)
}
if m.Get(ChannelWechat) != nil {
t.Error("空配置不应启用渠道")
}
if ok, reason := m.Status(ChannelWechat); ok || reason != "未配置" {
t.Errorf("空配置应报未配置,得 ok=%v reason=%q", ok, reason)
}
}
// ReloadWechat 配置不全:返回错误、渠道隐藏、原因列出缺失字段。
func TestManager_ReloadWechat_Incomplete(t *testing.T) {
m := NewManager()
err := m.ReloadWechat(context.Background(), Config{MchID: "123"}) // 只填一个字段
if err == nil {
t.Error("配置不全应返回错误")
}
if m.Get(ChannelWechat) != nil {
t.Error("配置不全不应启用渠道")
}
if _, reason := m.Status(ChannelWechat); reason == "" {
t.Error("配置不全应给出缺失原因")
}
}