252 lines
8.1 KiB
Go
252 lines
8.1 KiB
Go
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 校验订单支付状态白名单判断:仅「已支付及之后」为已支付,
|
||
// 未支付/已退款/已关闭等一律按未支付(fail-closed)。
|
||
func TestIsOrderPaidStatus(t *testing.T) {
|
||
cases := []struct {
|
||
status int
|
||
want bool
|
||
}{
|
||
{XPayOrderStatusPaid, true},
|
||
{XPayOrderStatusProviding, true},
|
||
{XPayOrderStatusProvided, true},
|
||
{XPayOrderStatusInit, false},
|
||
{XPayOrderStatusCreated, false},
|
||
{XPayOrderStatusRefunded, false},
|
||
{XPayOrderStatusClosed, false},
|
||
{XPayOrderStatusRefundFailed, false},
|
||
}
|
||
for _, c := range cases {
|
||
if got := IsOrderPaidStatus(c.status); got != c.want {
|
||
t.Fatalf("IsOrderPaidStatus(%d) = %v, want %v", c.status, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestIsOrderRefundedStatus 校验退款状态白名单判断:仅已退款/用户退款完成为已退款。
|
||
func TestIsOrderRefundedStatus(t *testing.T) {
|
||
cases := []struct {
|
||
status int
|
||
want bool
|
||
}{
|
||
{XPayOrderStatusRefunded, true},
|
||
{XPayOrderStatusUserRefunded, true},
|
||
{XPayOrderStatusPaid, false},
|
||
{XPayOrderStatusRefundFailed, false},
|
||
{XPayOrderStatusCreated, false},
|
||
}
|
||
for _, c := range cases {
|
||
if got := IsOrderRefundedStatus(c.status); got != c.want {
|
||
t.Fatalf("IsOrderRefundedStatus(%d) = %v, want %v", c.status, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestXPayOrderInfoUnmarshal 校验 query_order 响应解析:订单字段包在 order 子对象内,status 为数字枚举。
|
||
// 回归防护:早期实现把字段平铺在顶层且 status 定义为 string,导致反查恒失败、订单永远停在待支付。
|
||
func TestXPayOrderInfoUnmarshal(t *testing.T) {
|
||
raw := []byte(`{
|
||
"errcode": 0,
|
||
"errmsg": "",
|
||
"order": {
|
||
"order_id": "OD20260914014559fc1cfee91b3748a2",
|
||
"wx_order_id": "wx_order_1",
|
||
"status": 2,
|
||
"order_fee": 990,
|
||
"paid_fee": 990,
|
||
"order_type": 0,
|
||
"refund_fee": 0,
|
||
"left_fee": 990,
|
||
"env_type": 2,
|
||
"biz_meta": "monthly_vip"
|
||
}
|
||
}`)
|
||
var info XPayOrderInfo
|
||
if err := json.Unmarshal(raw, &info); err != nil {
|
||
t.Fatalf("unmarshal query_order: %v", err)
|
||
}
|
||
if info.Order.Status != XPayOrderStatusPaid {
|
||
t.Fatalf("status = %d, want %d", info.Order.Status, XPayOrderStatusPaid)
|
||
}
|
||
if info.Order.PaidFee != 990 {
|
||
t.Fatalf("paid_fee = %d, want 990", info.Order.PaidFee)
|
||
}
|
||
if info.Order.OrderID != "OD20260914014559fc1cfee91b3748a2" {
|
||
t.Fatalf("order_id = %q", info.Order.OrderID)
|
||
}
|
||
if !IsOrderPaidStatus(info.Order.Status) {
|
||
t.Fatalf("IsOrderPaidStatus(%d) = false, want true", info.Order.Status)
|
||
}
|
||
}
|
||
|
||
// 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")
|
||
}
|
||
}
|