a6b068b070
照 alipay adapter 的 RSA + redirect/qr + 表单请求模式实现哪吒(nzzf.org)聚合支付: SHA256WithRSA + Base64 签名(商户私钥出站签名/平台公钥验响应及回调),Create 走 POST /api/pay/create,VerifyCallback 处理 GET 异步通知(handler 层新增 Query 透传 + nezha 专属纯文本 success/fail 回复),Query 走 POST /api/pay/query。BuildRegistry 按凭证齐备与否决策注册,缺凭证只 log 跳过不 fatal。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
192 lines
6.6 KiB
Go
192 lines
6.6 KiB
Go
package providerbuild_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/wangjia/pay/config"
|
|
"github.com/wangjia/pay/internal/accounts"
|
|
"github.com/wangjia/pay/internal/providerbuild"
|
|
)
|
|
|
|
// TestBuildRegistry 表驱动覆盖 BuildRegistry 的装配/跳过决策:据 accounts.Registry
|
|
// 的 enabled 账户 + 对应 env 凭证是否齐备,决定各渠道是否 Register——缺凭证只
|
|
// log+skip,绝不 fatal(允许只上线部分渠道)。
|
|
//
|
|
// alipay 的"凭证齐备→真注册"分支这里不覆盖(需要构造能通过
|
|
// x509.MarshalPKIXPublicKey/LoadAliPayPublicKey 的 RSA 密钥对,详见
|
|
// internal/provider/alipay/alipay_test.go 的 genKeys——挪来此处会让本测试文件
|
|
// 显著变重,且该构造逻辑本身已被 alipay 包自己的单测覆盖)。这里只验证 alipay
|
|
// 的"缺凭证→跳过、不 fatal、注册表里没有它"分支,以及它在混合场景里不会误伤
|
|
// 同批次里凭证齐备的其它渠道——这正是 BuildRegistry 装配决策本身要保证的行为。
|
|
func TestBuildRegistry(t *testing.T) {
|
|
// nezha 凭证解析(export_test.go 的 base64 DER 兜底路径,见 nezha 包)不像 alipay
|
|
// 依赖 smartwalle SDK 的特定加载方式,构造一对测试用 RSA 密钥成本很低,故这里同时
|
|
// 覆盖"凭证齐备→真注册"分支(alipay 因构造成本高特意跳过,见上方注释)。
|
|
nezhaMerchant, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("gen nezha merchant key: %v", err)
|
|
}
|
|
nezhaPlatform, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("gen nezha platform key: %v", err)
|
|
}
|
|
nezhaPrivB64 := base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(nezhaMerchant))
|
|
nezhaPubDER, err := x509.MarshalPKIXPublicKey(&nezhaPlatform.PublicKey)
|
|
if err != nil {
|
|
t.Fatalf("marshal nezha platform pub: %v", err)
|
|
}
|
|
nezhaPubB64 := base64.StdEncoding.EncodeToString(nezhaPubDER)
|
|
|
|
cases := []struct {
|
|
name string
|
|
accounts []config.AccountConfig
|
|
env map[string]string
|
|
want []string // 期望注册的 Method() 集合
|
|
}{
|
|
{
|
|
name: "无任何 enabled 账户_空注册表",
|
|
accounts: nil,
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "crypto_enabled_且凭证齐备_注册crypto",
|
|
accounts: []config.AccountConfig{
|
|
{AccountID: "cry-1", Channel: "crypto", Enabled: true, CredentialEnvPrefix: "t_cry"},
|
|
},
|
|
env: map[string]string{
|
|
"T_CRY_ADDRESS": "TWtest0000000000000000000000000000",
|
|
"T_CRY_TRONGRID_KEY": "k",
|
|
},
|
|
want: []string{"crypto"},
|
|
},
|
|
{
|
|
name: "alipay_enabled_但凭证不全_跳过不fatal_注册表无alipay",
|
|
accounts: []config.AccountConfig{
|
|
{AccountID: "ali-1", Channel: "alipay", Enabled: true, CredentialEnvPrefix: "t_ali"},
|
|
},
|
|
// 故意不设 T_ALI_APP_ID / _APP_PRIVATE_KEY / _ALIPAY_PUBLIC_KEY:
|
|
// 若 BuildRegistry 对此 fatal/panic,本测试直接挂掉,已是断言的一部分。
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "nezha_enabled_但凭证不全_跳过不fatal_注册表无nezha",
|
|
accounts: []config.AccountConfig{
|
|
{AccountID: "nz-1", Channel: "nezha", Enabled: true, CredentialEnvPrefix: "t_nz"},
|
|
},
|
|
// 故意不设 T_NZ_PID/_PRIVATE_KEY/_PLATFORM_PUBLIC_KEY。
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "nezha_enabled_且凭证齐备_注册nezha",
|
|
accounts: []config.AccountConfig{
|
|
{AccountID: "nz-2", Channel: "nezha", Enabled: true, CredentialEnvPrefix: "t_nz2"},
|
|
},
|
|
env: map[string]string{
|
|
"T_NZ2_PID": "test-pid",
|
|
"T_NZ2_PRIVATE_KEY": nezhaPrivB64,
|
|
"T_NZ2_PLATFORM_PUBLIC_KEY": nezhaPubB64,
|
|
},
|
|
want: []string{"nezha"},
|
|
},
|
|
{
|
|
name: "stripe_enabled_且凭证齐备_注册stripe",
|
|
accounts: []config.AccountConfig{
|
|
{AccountID: "st-1", Channel: "stripe", Enabled: true, CredentialEnvPrefix: "t_st"},
|
|
},
|
|
env: map[string]string{
|
|
"T_ST_SECRET_KEY": "sk_test_x",
|
|
"T_ST_WEBHOOK_SECRET": "whsec_x",
|
|
},
|
|
want: []string{"stripe"},
|
|
},
|
|
{
|
|
name: "混合_一个凭证齐备一个不全_只注册齐备的那个",
|
|
accounts: []config.AccountConfig{
|
|
{AccountID: "st-2", Channel: "stripe", Enabled: true, CredentialEnvPrefix: "t_st2"},
|
|
{AccountID: "ali-2", Channel: "alipay", Enabled: true, CredentialEnvPrefix: "t_ali2"},
|
|
},
|
|
env: map[string]string{
|
|
"T_ST2_SECRET_KEY": "sk_test_y",
|
|
"T_ST2_WEBHOOK_SECRET": "whsec_y",
|
|
// ali-2 故意留白凭证。
|
|
},
|
|
want: []string{"stripe"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for k, v := range tc.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
acctReg := accounts.New(tc.accounts)
|
|
reg := providerbuild.BuildRegistry(acctReg)
|
|
|
|
got := reg.Methods()
|
|
sort.Strings(got)
|
|
want := append([]string(nil), tc.want...)
|
|
sort.Strings(want)
|
|
if !reflect.DeepEqual(got, want) && !(len(got) == 0 && len(want) == 0) {
|
|
t.Fatalf("Methods() = %v, want %v", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildRegistryMockChannelEnabled 覆盖 config.C.MockChannelEnabled=true 时的
|
|
// fake 装配分支:注册表含 "fake",且其结算币种被设成 ["CNY","USDT"](CNY 排首位,
|
|
// 对齐 pangolin 下单实际结算币种,见 registry.go 顶部注释)。config.C 是包级全局,
|
|
// 测试内直接改字段 + defer 复原,避免污染同包/其它包测试的默认值(与
|
|
// handler 包 ratelimit_test.go / refund_test.go 的 config.C 约定一致)。
|
|
func TestBuildRegistryMockChannelEnabled(t *testing.T) {
|
|
prev := config.C.MockChannelEnabled
|
|
config.C.MockChannelEnabled = true
|
|
defer func() { config.C.MockChannelEnabled = prev }()
|
|
|
|
acctReg := accounts.New(nil)
|
|
reg := providerbuild.BuildRegistry(acctReg)
|
|
|
|
methods := reg.Methods()
|
|
found := false
|
|
for _, m := range methods {
|
|
if m == "fake" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("MockChannelEnabled=true 时 Methods() 应含 fake, got %v", methods)
|
|
}
|
|
|
|
fp, err := reg.Get("fake")
|
|
if err != nil {
|
|
t.Fatalf("Get(fake): %v", err)
|
|
}
|
|
caps := fp.Capabilities()
|
|
if len(caps.SettleCurrencies) == 0 || caps.SettleCurrencies[0] != "CNY" {
|
|
t.Fatalf("mock 装配应把 fake 结算币种设成 CNY 优先, got %v", caps.SettleCurrencies)
|
|
}
|
|
}
|
|
|
|
// TestBuildRegistryMockChannelDisabledByDefault 反向覆盖:MockChannelEnabled=false
|
|
// (生产默认值)时注册表不含 "fake",证明 mock 渠道对生产零影响。
|
|
func TestBuildRegistryMockChannelDisabledByDefault(t *testing.T) {
|
|
prev := config.C.MockChannelEnabled
|
|
config.C.MockChannelEnabled = false
|
|
defer func() { config.C.MockChannelEnabled = prev }()
|
|
|
|
acctReg := accounts.New(nil)
|
|
reg := providerbuild.BuildRegistry(acctReg)
|
|
|
|
for _, m := range reg.Methods() {
|
|
if m == "fake" {
|
|
t.Fatalf("MockChannelEnabled=false(默认)时 Methods() 不应含 fake, got %v", reg.Methods())
|
|
}
|
|
}
|
|
}
|