3a4e1d53a5
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>
97 lines
3.4 KiB
Go
97 lines
3.4 KiB
Go
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
|
||
}
|
||
|
||
// 空 Manager:Get 任意渠道为 nil,Available 为空,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("配置不全应给出缺失原因")
|
||
}
|
||
}
|