Files
zt-closet-backend/pkg/wechat/virtualpay_test.go
T
2026-09-11 01:41:52 +08:00

192 lines
6.2 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 wechat
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"sort"
"strings"
"testing"
)
// TestHMACSHA256Hex 用 RFC 4231 测试向量校验 HMAC-SHA256 实现。
func TestHMACSHA256Hex(t *testing.T) {
key := strings.Repeat("\x0b", 20) // 0x0b * 20
data := "Hi There"
want := "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
if got := hmacSHA256Hex(key, data); got != want {
t.Fatalf("hmacSHA256Hex mismatch: got %s, want %s", got, want)
}
}
// TestBuildPaymentSign 校验 signData 字典序、paySig 与 signature 的内部一致性。
func TestBuildPaymentSign(t *testing.T) {
const (
appKey = "test-app-key"
sessionKey = "test-session-key"
offerID = "wx1234567890"
productID = "monthly_vip"
outTradeNo = "OD20260908120000abcdef"
attach = "uid=42"
)
virtualPayMu.Lock()
defaultVirtualPayClient = &virtualPayClient{appKey: appKey, env: XPayEnvProd}
virtualPayMu.Unlock()
defer func() {
virtualPayMu.Lock()
defaultVirtualPayClient = nil
virtualPayMu.Unlock()
}()
sign, err := BuildPaymentSign(offerID, productID, outTradeNo, attach, sessionKey, 1900)
if err != nil {
t.Fatalf("BuildPaymentSign error: %v", err)
}
// signData 字段必须按字典序排列(微信侧按字典序重组验签)。
wantOrder := []string{"attach", "buyQuantity", "currencyType", "env", "goodsPrice", "offerId", "outTradeNo", "productId"}
last := -1
for _, key := range wantOrder {
idx := strings.Index(sign.SignData, `"`+key+`"`)
if idx < 0 {
t.Fatalf("signData missing key %q: %s", key, sign.SignData)
}
if idx < last {
t.Fatalf("signData key order wrong at %q: %s", key, sign.SignData)
}
last = idx
}
// signData 必须是合法 JSON,且 offerId 为字符串。
var m map[string]any
if err := json.Unmarshal([]byte(sign.SignData), &m); err != nil {
t.Fatalf("signData is not valid JSON: %v", err)
}
if _, ok := m["offerId"].(string); !ok {
t.Fatalf("offerId must be string, got %T", m["offerId"])
}
if v, ok := m["goodsPrice"].(float64); !ok || v != 1900 {
t.Fatalf("goodsPrice mismatch: %v", m["goodsPrice"])
}
// paySig = hmac(appKey, "requestVirtualPayment&"+signData);signature = hmac(sessionKey, signData)。
wantPaySig := hmacSHA256Hex(appKey, requestVirtualPaymentURI+"&"+sign.SignData)
wantSignature := hmacSHA256Hex(sessionKey, sign.SignData)
if sign.PaySig != wantPaySig {
t.Fatalf("paySig mismatch: got %s, want %s", sign.PaySig, wantPaySig)
}
if sign.Signature != wantSignature {
t.Fatalf("signature mismatch: got %s, want %s", sign.Signature, wantSignature)
}
}
// TestCheckURLSignature 校验消息推送 URL 验签(SHA1 字典序拼接)。
func TestCheckURLSignature(t *testing.T) {
token := "my-token"
timestamp := "1756000000"
nonce := "abc123"
// 独立计算期望值:["1756000000","abc123","my-token"] 字典序拼接后做 SHA1。
arr := []string{token, timestamp, nonce}
sort.Strings(arr)
sum := sha1.Sum([]byte(arr[0] + arr[1] + arr[2]))
wantSig := hex.EncodeToString(sum[:])
if !CheckURLSignature(token, timestamp, nonce, wantSig) {
t.Fatal("CheckURLSignature should pass with correct signature")
}
if CheckURLSignature(token, timestamp, nonce, "bad") {
t.Fatal("CheckURLSignature should fail with wrong signature")
}
if CheckURLSignature("", timestamp, nonce, wantSig) {
t.Fatal("CheckURLSignature should fail with empty token")
}
}
// TestParseNotifyAndResponse 校验发货推送解析与应答构造。
func TestParseNotifyAndResponse(t *testing.T) {
raw := []byte(`{
"ToUserName":"gh_test",
"FromUserName":"official_openid",
"CreateTime":1756000000,
"MsgType":"event",
"Event":"xpay_goods_deliver_notify",
"OpenId":"o_user",
"OutTradeNo":"OD20260908120000abcdef",
"Env":0,
"WeChatPayInfo":{"MchOrderNo":"mch_123","TransactionId":"wx_123","PaidTime":1756000000},
"GoodsInfo":{"ProductId":"monthly_vip","Quantity":1,"OrigPrice":1900,"ActualPrice":1900,"Attach":"uid=42"}
}`)
base, err := ParseNotify(raw)
if err != nil {
t.Fatalf("ParseNotify error: %v", err)
}
if base.Event != EventGoodsDeliverNotify {
t.Fatalf("unexpected event: %s", base.Event)
}
if base.OutTradeNo != "OD20260908120000abcdef" {
t.Fatalf("unexpected out_trade_no: %s", base.OutTradeNo)
}
var deliver XPayGoodsDeliverNotify
if err := json.Unmarshal(raw, &deliver); err != nil {
t.Fatalf("unmarshal deliver notify: %v", err)
}
if deliver.GoodsInfo.ProductID != "monthly_vip" || deliver.GoodsInfo.ActualPrice != 1900 {
t.Fatalf("deliver goods info mismatch: %+v", deliver.GoodsInfo)
}
okBody := BuildNotifyResponse(true)
if !strings.Contains(string(okBody), `"ErrCode":0`) {
t.Fatalf("success response missing ErrCode 0: %s", okBody)
}
failBody := BuildNotifyResponse(false)
if !strings.Contains(string(failBody), `"ErrCode":1`) {
t.Fatalf("fail response missing ErrCode 1: %s", failBody)
}
}
// TestIsOrderPaidStatus 校验订单支付状态白名单判断:仅已知成功枚举为已支付,未知/空串按未支付。
func TestIsOrderPaidStatus(t *testing.T) {
cases := []struct {
status string
want bool
}{
{XPayOrderStatusPaid, true},
{"NOTPAY", false},
{"CLOSED", false},
{"UNKNOWN", false},
{"", false},
}
for _, c := range cases {
if got := IsOrderPaidStatus(c.status); got != c.want {
t.Fatalf("IsOrderPaidStatus(%q) = %v, want %v", c.status, got, c.want)
}
}
}
// TestResolveEnvKey 校验环境自动选择规则:只填现网/只填沙箱/两把都填/两把都空。
func TestResolveEnvKey(t *testing.T) {
// 只填现网 -> 现网
key, env, err := resolveEnvKey("prod-key", "")
if err != nil || key != "prod-key" || env != XPayEnvProd {
t.Fatalf("only env key: key=%q env=%d err=%v", key, env, err)
}
// 只填沙箱 -> 沙箱
key, env, err = resolveEnvKey("", "sandbox-key")
if err != nil || key != "sandbox-key" || env != XPayEnvSandbox {
t.Fatalf("only sandbox key: key=%q env=%d err=%v", key, env, err)
}
// 两把都填 -> 现网优先
key, env, err = resolveEnvKey("prod-key", "sandbox-key")
if err != nil || key != "prod-key" || env != XPayEnvProd {
t.Fatalf("both keys: key=%q env=%d err=%v", key, env, err)
}
// 两把都空 -> 报错
if _, _, err := resolveEnvKey("", ""); err == nil {
t.Fatal("empty keys should return error")
}
}