merge: 补测试批B(settle并发/全链e2e/reconcile装配/alipay健壮性/money零小数位)
This commit is contained in:
@@ -178,6 +178,189 @@ func TestVerifyCallbackRSA_ClosedIsFailed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 健壮性测试(补测项 ⑨):以上验签测试都是自生成密钥对自签自验(测的是我方 adapter
|
||||
// 逻辑本身,不是支付宝真实签名细节);本节仍是自签,但专挑真实回调报文常见的
|
||||
// "互操作易碎点"——未知/多余字段、非字母序的字段排列、中文/unicode 值、可选字段
|
||||
// 留空。⚠️ 真实支付宝黄金向量(官方真实回调报文的逐字节样本)需要抓真实线上/沙箱
|
||||
// 回调抓包,本文件不具备,以下均是自签构造。
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestVerifyCallbackRSA_UnknownExtraFieldsIgnored 真实支付宝回调常带一大堆我方代码
|
||||
// 不读取的字段(app_id/seller_email/buyer_logon_id/notify_id/charset/version/
|
||||
// receipt_amount/point_amount...)。signRSA2 与 SDK 内部验签都按官方规则排除
|
||||
// sign/sign_type 及空值字段参与签名重算,多余字段不应打断验签,也不应污染归一化
|
||||
// 结果(ProviderRef/Status 只认 out_trade_no/trade_status/total_amount)。
|
||||
func TestVerifyCallbackRSA_UnknownExtraFieldsIgnored(t *testing.T) {
|
||||
appPriv, aliPriv, aliPub := genKeys(t)
|
||||
p := ali.New(buildClient(t, appPriv, aliPub))
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("out_trade_no", "PAY-EXTRA")
|
||||
form.Set("trade_no", "2021EXTRA")
|
||||
form.Set("trade_status", "TRADE_SUCCESS")
|
||||
form.Set("total_amount", "1.00")
|
||||
form.Set("sign_type", "RSA2")
|
||||
// 支付宝真实通知常见的额外字段,adapter 均不读取,只应被忽略。
|
||||
form.Set("app_id", "2021000000000000")
|
||||
form.Set("auth_app_id", "2021000000000000")
|
||||
form.Set("notify_id", "notify-xyz")
|
||||
form.Set("notify_type", "trade_status_sync")
|
||||
form.Set("notify_time", "2026-07-10 15:04:05")
|
||||
form.Set("charset", "utf-8")
|
||||
form.Set("version", "1.0")
|
||||
form.Set("seller_id", "2088seller")
|
||||
form.Set("seller_email", "seller@example.com")
|
||||
form.Set("buyer_id", "2088buyer")
|
||||
form.Set("buyer_logon_id", "buy***@example.com")
|
||||
form.Set("receipt_amount", "1.00")
|
||||
form.Set("point_amount", "0.00")
|
||||
form.Set("sign", signRSA2(t, aliPriv, form))
|
||||
|
||||
ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{Raw: []byte(form.Encode())})
|
||||
if err != nil {
|
||||
t.Fatalf("verify: %v", err)
|
||||
}
|
||||
if ev.ProviderRef != "PAY-EXTRA" || ev.Status != provider.PaidSucceeded || ev.PaidAmountMinor != 100 {
|
||||
t.Fatalf("event = %+v", ev)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyCallbackRSA_FieldOrderIndependent 真实网关/CDN/负载均衡转发不保证 query
|
||||
// 字段在线路上的排列顺序;VerifyCallback 先 url.ParseQuery 解成 url.Values(map),
|
||||
// 理论上字段顺序不该影响结果——这里故意手拼一份与字母序相反的 wire bytes(而非用
|
||||
// form.Encode() 那种天然按 key 字母序输出的路径)来钉住这一互操作预期。
|
||||
func TestVerifyCallbackRSA_FieldOrderIndependent(t *testing.T) {
|
||||
appPriv, aliPriv, aliPub := genKeys(t)
|
||||
p := ali.New(buildClient(t, appPriv, aliPub))
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("out_trade_no", "PAY-ORDER")
|
||||
form.Set("trade_no", "2021ORDER")
|
||||
form.Set("trade_status", "TRADE_SUCCESS")
|
||||
form.Set("total_amount", "1.00")
|
||||
form.Set("gmt_payment", "2026-07-10 15:04:05")
|
||||
form.Set("sign_type", "RSA2")
|
||||
sign := signRSA2(t, aliPriv, form) // 按官方排序算出的签名,与 wire 上字段实际排列顺序无关
|
||||
|
||||
raw := "sign_type=" + url.QueryEscape("RSA2") +
|
||||
"&trade_status=" + url.QueryEscape("TRADE_SUCCESS") +
|
||||
"&total_amount=" + url.QueryEscape("1.00") +
|
||||
"&gmt_payment=" + url.QueryEscape("2026-07-10 15:04:05") +
|
||||
"&out_trade_no=" + url.QueryEscape("PAY-ORDER") +
|
||||
"&trade_no=" + url.QueryEscape("2021ORDER") +
|
||||
"&sign=" + url.QueryEscape(sign)
|
||||
|
||||
ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{Raw: []byte(raw)})
|
||||
if err != nil {
|
||||
t.Fatalf("verify(乱序字段): %v", err)
|
||||
}
|
||||
if ev.ProviderRef != "PAY-ORDER" || ev.Status != provider.PaidSucceeded {
|
||||
t.Fatalf("event = %+v", ev)
|
||||
}
|
||||
if ev.PaidAt == nil {
|
||||
t.Fatal("乱序字段不应影响 gmt_payment 解析,PaidAt 不应为 nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyCallbackRSA_UnicodeSubject subject/body 携带中文 + emoji(真实商品名称/
|
||||
// 备注常见),须能正确 percent-encode/decode 并参与签名而不破坏验签或后续解析。
|
||||
func TestVerifyCallbackRSA_UnicodeSubject(t *testing.T) {
|
||||
appPriv, aliPriv, aliPub := genKeys(t)
|
||||
p := ali.New(buildClient(t, appPriv, aliPub))
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("out_trade_no", "PAY-CN")
|
||||
form.Set("trade_no", "2021CN")
|
||||
form.Set("trade_status", "TRADE_SUCCESS")
|
||||
form.Set("total_amount", "1.00")
|
||||
form.Set("subject", "Pro 年付 🎉 商品名称含中文与emoji")
|
||||
form.Set("body", "订单备注:测试中文正文——含标点、全角符号「」")
|
||||
form.Set("sign_type", "RSA2")
|
||||
form.Set("sign", signRSA2(t, aliPriv, form))
|
||||
|
||||
ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{Raw: []byte(form.Encode())})
|
||||
if err != nil {
|
||||
t.Fatalf("verify: %v", err)
|
||||
}
|
||||
if ev.ProviderRef != "PAY-CN" || ev.Status != provider.PaidSucceeded || ev.PaidAmountMinor != 100 {
|
||||
t.Fatalf("event = %+v", ev)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyCallbackRSA_EmptyValueFieldsAreIncludedInSignature 补测中意外挖出的真实
|
||||
// SDK 行为,与"官方文档说空值参数不参与签名"的字面印象不一致,记录在案(避免下次
|
||||
// 被当成 bug 重新踩一遍):github.com/smartwalle/alipay/v3 注入给签名器的是它自己
|
||||
// vendor 的 Encoder(v3@v3.2.29/encode.go),不是 nsign 包默认的 DefaultEncoder——
|
||||
// 前者对"空值字段"**不做排除**,只要 key 不在 ignore 名单(sign/sign_type/
|
||||
// alipay_cert_sn)里,即便 value 是空字符串也会以 "key=" 形式进入签名源参与 hash;
|
||||
// nsign.DefaultEncoder 则会跳过空值。
|
||||
//
|
||||
// 实测过程:先按"官方文档描述的排除空值"假设写了个用例(签名时排除空值、wire 上
|
||||
// 带 3 个空值可选字段),结果 SDK 侧验签失败(crypto/rsa: verification error);手工
|
||||
// 复算签名源字符串确认两侧字符串其实一致后,才定位到问题出在 vendor Encoder 与
|
||||
// nsign.DefaultEncoder 的语义差异,而非我方 adapter 或字段解析逻辑的 bug。
|
||||
//
|
||||
// 本用例改为验证 SDK 的**真实行为**:空值字段必须参与签名源才能验签通过(signRSA2
|
||||
// 复刻的是"排除空值"这一支付宝官方文档口径,与此处刻意用 signIncludingEmpty 区分)。
|
||||
// 真实支付宝服务器是否会在回调里真的带上空值字段未经抓包验证,但只要它带了,
|
||||
// 当前这版 SDK 要求空值也进签名源——这条钉住的是 SDK 版本的真实契约,供后续升级
|
||||
// SDK 或换实现时对照。
|
||||
func TestVerifyCallbackRSA_EmptyValueFieldsAreIncludedInSignature(t *testing.T) {
|
||||
appPriv, aliPriv, aliPub := genKeys(t)
|
||||
p := ali.New(buildClient(t, appPriv, aliPub))
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("out_trade_no", "PAY-EMPTY")
|
||||
form.Set("trade_no", "2021EMPTY")
|
||||
form.Set("trade_status", "TRADE_SUCCESS")
|
||||
form.Set("total_amount", "1.00")
|
||||
form.Set("sign_type", "RSA2")
|
||||
form.Set("refund_status", "")
|
||||
form.Set("invoice_amount", "")
|
||||
form.Set("passback_params", "")
|
||||
form.Set("sign", signIncludingEmpty(t, aliPriv, form))
|
||||
|
||||
ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{Raw: []byte(form.Encode())})
|
||||
if err != nil {
|
||||
t.Fatalf("verify(含空值可选字段): %v", err)
|
||||
}
|
||||
if ev.ProviderRef != "PAY-EMPTY" || ev.Status != provider.PaidSucceeded {
|
||||
t.Fatalf("event = %+v", ev)
|
||||
}
|
||||
}
|
||||
|
||||
// signIncludingEmpty 复刻 alipay 包 vendor Encoder(见上面测试注释,非 nsign 默认
|
||||
// DefaultEncoder)的真实签名规则:排序**全部**非 sign/sign_type 字段(空值也算入),
|
||||
// k=v&拼接,RSA-SHA256,base64。与本文件 signRSA2(严格照官方文档排除空值)刻意
|
||||
// 区分,专供本测试还原 SDK 真实行为,不共用逻辑以免掩盖两者的语义差异。
|
||||
func signIncludingEmpty(t *testing.T, aliPrivB64 string, form url.Values) string {
|
||||
t.Helper()
|
||||
der, _ := base64.StdEncoding.DecodeString(aliPrivB64)
|
||||
priv, err := x509.ParsePKCS1PrivateKey(der)
|
||||
if err != nil {
|
||||
t.Fatalf("parse ali priv: %v", err)
|
||||
}
|
||||
keys := make([]string, 0, len(form))
|
||||
for k := range form {
|
||||
if k == "sign" || k == "sign_type" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+"="+form.Get(k))
|
||||
}
|
||||
h := sha256.Sum256([]byte(strings.Join(parts, "&")))
|
||||
sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h[:])
|
||||
if err != nil {
|
||||
t.Fatalf("sign: %v", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(sig)
|
||||
}
|
||||
|
||||
// signRSA2 复刻支付宝签名:排序非空参数(排除 sign/sign_type),k=v&拼接,RSA-SHA256,base64。
|
||||
func signRSA2(t *testing.T, aliPrivB64 string, form url.Values) string {
|
||||
t.Helper()
|
||||
|
||||
Reference in New Issue
Block a user