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