后端: - 新增 payment_configs 单行配置表;APIv3 密钥、应用私钥/公钥以 AES-256-GCM 密文入库(enc:v1 前缀,主密钥取 PAYMENT_SECRET_KEY,缺省回退 JWT_SECRET 派生) - 证书/私钥落 PAYMENT_CERT_DIR(默认 /data/payment-certs),目录 0700 / 文件 0600,不经任何静态路由暴露 - 接口:GET/PUT /api/admin/payment/config、POST /upload(PEM 校验 + 微信证书序列号自动回填)、POST /test(真打官方接口:微信 /v3/certificates 签名校验、支付宝 alipay.trade.query 签名校验)、POST /wechat/platform-certs(APIv3 密钥解密平台证书并落盘) - 未配置凭据时测试返回 skipped 而不是 500;敏感字段在返回体里一律脱敏 前端: - /payments 改为「通用设置 + 微信支付 / 支付宝 / Waffo 海外支付」页签(原 Waffo 页整页迁到 views/payments/Waffo.vue) - 微信:mchid、AppID、APIv3 密钥(密码框 + 掩码回显 + 更换/清除)、证书序列号、支付方式多选、三项证书上传、自动获取平台证书 - 支付宝:app_id、应用私钥(粘贴/上传)、支付宝公钥、网关、RSA2、回调地址 - 菜单归入「审计与设置 → 支付配置」
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package payment
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestMain 固定测试用加密主密钥(masterKey 只会解析一次) // Logan.Li
|
|
func TestMain(m *testing.M) {
|
|
os.Setenv(EnvPaymentMasterKey, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
|
os.Setenv("PAYMENT_CERT_DIR", os.TempDir()+"/opsx-licence-test-certs")
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// TestEncryptDecryptRoundTrip 验证 AES-GCM 加解密与密文格式 // Logan.Li
|
|
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
|
plain := "wx-api-v3-key-32-bytes-example!!"
|
|
enc, err := EncryptSecret(plain)
|
|
if err != nil {
|
|
t.Fatalf("加密失败: %v", err)
|
|
}
|
|
if !strings.HasPrefix(enc, cipherPrefix) {
|
|
t.Fatalf("密文缺少前缀: %s", enc)
|
|
}
|
|
if strings.Contains(enc, plain) {
|
|
t.Fatalf("密文中出现了明文")
|
|
}
|
|
got, err := DecryptSecret(enc)
|
|
if err != nil {
|
|
t.Fatalf("解密失败: %v", err)
|
|
}
|
|
if got != plain {
|
|
t.Fatalf("解密结果不一致: %q", got)
|
|
}
|
|
|
|
// 同一明文两次加密应产生不同密文(随机 nonce) // Logan.Li
|
|
enc2, _ := EncryptSecret(plain)
|
|
if enc == enc2 {
|
|
t.Fatalf("两次加密结果相同,nonce 未随机化")
|
|
}
|
|
}
|
|
|
|
// TestEncryptSecretEmpty 空值不应产生密文 // Logan.Li
|
|
func TestEncryptSecretEmpty(t *testing.T) {
|
|
enc, err := EncryptSecret(" ")
|
|
if err != nil || enc != "" {
|
|
t.Fatalf("空值加密应返回空串,得到 %q err=%v", enc, err)
|
|
}
|
|
if v, err := DecryptSecret(""); err != nil || v != "" {
|
|
t.Fatalf("空密文应返回空串,得到 %q err=%v", v, err)
|
|
}
|
|
}
|
|
|
|
// TestDecryptTampered 篡改密文必须解密失败 // Logan.Li
|
|
func TestDecryptTampered(t *testing.T) {
|
|
enc, _ := EncryptSecret("secret-value")
|
|
tampered := enc[:len(enc)-4] + "AAAA"
|
|
if _, err := DecryptSecret(tampered); err == nil {
|
|
t.Fatalf("篡改后的密文不应解密成功")
|
|
}
|
|
}
|
|
|
|
// TestMaskTail 脱敏仅保留末位 // Logan.Li
|
|
func TestMaskTail(t *testing.T) {
|
|
masked := MaskTail("0123456789abcdef0123456789abcdef", 4, 32)
|
|
if !strings.HasSuffix(masked, "cdef") {
|
|
t.Fatalf("脱敏结果应保留后 4 位: %s", masked)
|
|
}
|
|
if len([]rune(masked)) != 32 {
|
|
t.Fatalf("脱敏长度应为 32: %d", len([]rune(masked)))
|
|
}
|
|
if MaskTail("", 4, 32) != "" {
|
|
t.Fatalf("空值脱敏应为空串")
|
|
}
|
|
}
|
|
|
|
// TestPayMethods 支付方式校验/去重与展示解析 // Logan.Li
|
|
func TestPayMethods(t *testing.T) {
|
|
got, err := normalizePayMethods([]string{"native", " h5 ", "native", "JSAPI"})
|
|
if err != nil {
|
|
t.Fatalf("合法支付方式不应报错: %v", err)
|
|
}
|
|
want := []string{"native", "h5", "jsapi"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("解析结果数量不符: %v", got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("解析结果不符: %v", got)
|
|
}
|
|
}
|
|
if _, err := normalizePayMethods([]string{"alipay"}); err == nil {
|
|
t.Fatalf("非法支付方式应报错")
|
|
}
|
|
if _, err := normalizePayMethods(nil); err == nil {
|
|
t.Fatalf("空支付方式应报错")
|
|
}
|
|
if def := splitPayMethods(""); len(def) != 1 || def[0] != "native" {
|
|
t.Fatalf("空串展示应回退 native: %v", def)
|
|
}
|
|
}
|
|
|
|
// TestNormalizePEM 裸 base64 自动补 PEM 头 // Logan.Li
|
|
func TestNormalizePEM(t *testing.T) {
|
|
out := NormalizePEM("QUJDREVGRw==", "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----")
|
|
if !strings.HasPrefix(out, "-----BEGIN PUBLIC KEY-----") || !strings.Contains(out, "QUJDREVGRw==") {
|
|
t.Fatalf("PEM 补全失败: %s", out)
|
|
}
|
|
}
|