first commit
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user