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