2026-07-07 18:05:02 +08:00

228 lines
8.9 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 config
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"strings"
"testing"
)
func TestLoadLocalEnablesOutboxMQ(t *testing.T) {
cfg, err := Load("../../configs/config.yaml")
if err != nil {
t.Fatalf("Load local config failed: %v", err)
}
// 本地默认发布普通钱包事实和实时红包事实,链路和 Docker/testbox/线上保持一致。
if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.WalletOutbox.Enabled || !cfg.RocketMQ.RealtimeOutbox.Enabled {
t.Fatalf("local config must enable wallet outbox MQ producers: %+v", cfg.RocketMQ)
}
if !cfg.OutboxWorker.Enabled || !cfg.RealtimeOutboxWorker.Enabled || !cfg.ProjectionWorker.Enabled {
t.Fatalf("local config must enable wallet outbox workers: outbox=%+v realtime=%+v projection=%+v", cfg.OutboxWorker, cfg.RealtimeOutboxWorker, cfg.ProjectionWorker)
}
if !cfg.ExternalRechargeReconcileWorker.Enabled || cfg.ExternalRechargeReconcileWorker.BatchSize != 50 || cfg.ExternalRechargeReconcileWorker.AppCode != "lalu" {
t.Fatalf("local config must enable external recharge reconcile worker: %+v", cfg.ExternalRechargeReconcileWorker)
}
if cfg.OutboxWorker.Concurrency != 1 || cfg.RealtimeOutboxWorker.Concurrency != 2 {
t.Fatalf("local outbox worker defaults mismatch: outbox=%+v realtime=%+v", cfg.OutboxWorker, cfg.RealtimeOutboxWorker)
}
if cfg.BSCUSDT.Enabled || cfg.ExternalRecharge.USDTBEP20Enabled {
t.Fatalf("bsc usdt receipt verification must stay disabled by default: bsc=%+v external=%+v", cfg.BSCUSDT, cfg.ExternalRecharge)
}
}
func TestLoadTencentExampleSplitsRealtimeOutboxAndRaisesThroughput(t *testing.T) {
cfg, err := Load("../../configs/config.tencent.example.yaml")
if err != nil {
t.Fatalf("Load tencent example failed: %v", err)
}
if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.WalletOutbox.Enabled || !cfg.RocketMQ.RealtimeOutbox.Enabled {
t.Fatalf("tencent example must enable both wallet outbox producers: %+v", cfg.RocketMQ)
}
if cfg.RocketMQ.WalletOutbox.Topic == cfg.RocketMQ.RealtimeOutbox.Topic {
t.Fatalf("realtime outbox topic must be separated from generic wallet topic: %+v", cfg.RocketMQ)
}
if cfg.OutboxWorker.BatchSize != 200 || cfg.OutboxWorker.Concurrency != 8 {
t.Fatalf("generic outbox throughput config mismatch: %+v", cfg.OutboxWorker)
}
if cfg.RealtimeOutboxWorker.BatchSize != 50 || cfg.RealtimeOutboxWorker.Concurrency != 4 {
t.Fatalf("realtime outbox worker config mismatch: %+v", cfg.RealtimeOutboxWorker)
}
if len(cfg.RealtimeOutboxWorker.EventTypes) != 3 || cfg.RealtimeOutboxWorker.EventTypes[0] != "WalletRedPacketCreated" {
t.Fatalf("realtime outbox event types mismatch: %+v", cfg.RealtimeOutboxWorker.EventTypes)
}
}
func TestBinanceConfigDefaultsAndAccountSelection(t *testing.T) {
cfg := normalizeBinanceConfig(BinanceConfig{
Lalu: BinanceAccountConfig{
APIKey: " lalu-key ",
APISecretKey: " lalu-secret ",
},
Aslan: BinanceAccountConfig{
APIKey: "aslan-key",
APISecretKey: "",
},
})
if cfg.APIBaseURL != "https://api.binance.com" || cfg.RecvWindow != 10000 || cfg.HTTPTimeout == 0 {
t.Fatalf("binance defaults mismatch: %+v", cfg)
}
if !cfg.HasAnyAccount() {
t.Fatalf("binance should detect at least one complete account")
}
lalu, ok := cfg.Account(" LALU ")
if !ok || lalu.APIKey != "lalu-key" || lalu.APISecretKey != "lalu-secret" {
t.Fatalf("lalu account selection mismatch: ok=%v account=%+v", ok, lalu)
}
if _, ok := cfg.Account("aslan"); ok {
t.Fatalf("incomplete aslan account must not be selectable")
}
if _, ok := cfg.Account("unknown"); ok {
t.Fatalf("unknown app must not fall back to lalu")
}
}
func TestExternalRechargeAddressMapNormalizesAppOverrides(t *testing.T) {
addresses := normalizeExternalRechargeAddressMap(map[string]string{
" ASLAN ": " TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J ",
"": "TEmptyApp",
"yumi": "",
})
if len(addresses) != 1 || addresses["aslan"] != "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J" {
t.Fatalf("address map normalization mismatch: %+v", addresses)
}
}
func TestLoadMifaPayMerchantConfig(t *testing.T) {
cases := []struct {
path string
env string
merNo string
merAccount string
wantKey bool
}{
{
path: "../../configs/config.yaml",
env: "test",
merNo: "10001925",
merAccount: "d00f5fc97a114a0db291eceaf9841739",
wantKey: true,
},
{
path: "../../configs/config.docker.yaml",
env: "test",
merNo: "10001925",
merAccount: "d00f5fc97a114a0db291eceaf9841739",
},
{
path: "../../configs/config.tencent.example.yaml",
env: "prod",
merNo: "10001910",
merAccount: "6000055d34f445418124ab7c0beb81a6",
},
}
for _, tc := range cases {
cfg, err := Load(tc.path)
if err != nil {
t.Fatalf("Load %s failed: %v", tc.path, err)
}
if cfg.MifaPay.Environment != tc.env || cfg.MifaPay.MerNo != tc.merNo || cfg.MifaPay.MerAccount != tc.merAccount {
t.Fatalf("mifapay merchant identity mismatch in %s: env=%q mer_no=%q mer_account=%q", tc.path, cfg.MifaPay.Environment, cfg.MifaPay.MerNo, cfg.MifaPay.MerAccount)
}
if tc.wantKey {
assertRSAPrivateKey(t, tc.path+" private_key", cfg.MifaPay.PrivateKey)
}
if strings.TrimSpace(cfg.MifaPay.MerchantPublicKey) != "" {
assertRSAPublicKey(t, tc.path+" merchant_public_key", cfg.MifaPay.MerchantPublicKey)
// MiFaPay 只保存商户公钥来验请求签名;本地配置同时持有私钥时,提前校验密钥对避免运行后才出现 Signature error。
if tc.wantKey {
assertRSAPrivateKeyMatchesPublicKey(t, tc.path+" mifapay keypair", cfg.MifaPay.PrivateKey, cfg.MifaPay.MerchantPublicKey)
}
}
assertRSAPublicKey(t, tc.path+" platform_public_key", cfg.MifaPay.PlatformPublicKey)
}
}
func TestLoadMifaPayTestEndpointKeyPair(t *testing.T) {
cfg, err := Load("../../configs/config.yaml")
if err != nil {
t.Fatalf("Load local config failed: %v", err)
}
testCfg := normalizeMifaPayConfig(MifaPayConfig{
Environment: "test",
EnvironmentTest: cfg.MifaPay.EnvironmentTest,
})
if testCfg.Environment != "test" || testCfg.MerNo != "10001925" || testCfg.MerAccount != "d00f5fc97a114a0db291eceaf9841739" {
t.Fatalf("mifapay test endpoint identity mismatch: %+v", testCfg)
}
assertRSAPrivateKey(t, "mifapay test private_key", testCfg.PrivateKey)
assertRSAPublicKey(t, "mifapay test merchant_public_key", testCfg.MerchantPublicKey)
// test 环境用于本地联调请求签名只用私钥merchant_public_key 作为后台上传值必须和私钥保持一致。
assertRSAPrivateKeyMatchesPublicKey(t, "mifapay test keypair", testCfg.PrivateKey, testCfg.MerchantPublicKey)
}
func assertRSAPrivateKey(t *testing.T, name string, value string) {
t.Helper()
clean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(value))
raw, err := base64.StdEncoding.DecodeString(clean)
if err != nil {
t.Fatalf("%s should be base64 DER private key: %v", name, err)
}
key, err := x509.ParsePKCS8PrivateKey(raw)
if err != nil {
t.Fatalf("%s should parse as PKCS8 private key: %v", name, err)
}
if _, ok := key.(*rsa.PrivateKey); !ok {
t.Fatalf("%s should be RSA private key, got %T", name, key)
}
}
func assertRSAPublicKey(t *testing.T, name string, value string) {
t.Helper()
clean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(value))
raw, err := base64.StdEncoding.DecodeString(clean)
if err != nil {
t.Fatalf("%s should be base64 DER public key: %v", name, err)
}
key, err := x509.ParsePKIXPublicKey(raw)
if err != nil {
t.Fatalf("%s should parse as PKIX public key: %v", name, err)
}
if _, ok := key.(*rsa.PublicKey); !ok {
t.Fatalf("%s should be RSA public key, got %T", name, key)
}
}
func assertRSAPrivateKeyMatchesPublicKey(t *testing.T, name string, privateValue string, publicValue string) {
t.Helper()
privateClean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(privateValue))
privateRaw, err := base64.StdEncoding.DecodeString(privateClean)
if err != nil {
t.Fatalf("%s private key should be base64 DER: %v", name, err)
}
privateParsed, err := x509.ParsePKCS8PrivateKey(privateRaw)
if err != nil {
t.Fatalf("%s private key should parse as PKCS8: %v", name, err)
}
privateKey, ok := privateParsed.(*rsa.PrivateKey)
if !ok {
t.Fatalf("%s private key should be RSA, got %T", name, privateParsed)
}
publicClean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(publicValue))
publicRaw, err := base64.StdEncoding.DecodeString(publicClean)
if err != nil {
t.Fatalf("%s public key should be base64 DER: %v", name, err)
}
publicParsed, err := x509.ParsePKIXPublicKey(publicRaw)
if err != nil {
t.Fatalf("%s public key should parse as PKIX: %v", name, err)
}
publicKey, ok := publicParsed.(*rsa.PublicKey)
if !ok {
t.Fatalf("%s public key should be RSA, got %T", name, publicParsed)
}
if privateKey.N.Cmp(publicKey.N) != 0 || privateKey.E != publicKey.E {
t.Fatalf("%s private key does not match merchant public key", name)
}
}