Files
ttstd-infinite-canvas/backend/payment-plugins/epay_merge_test.go
ddCat 3e35f7d687 fix(merge): PR 集成 - fix merge 上传状态与易支付校验
在 #544、#545、#546 的原作者 merge commit 之后独立修复:保留上传 Promise、处理重复上传与复制失败、格式对齐,以及易支付网关、订单证据、签名、通知幂等与安全关单校验。同步重建六平台插件产物和包。

验证:前端 1832 tests、build/typecheck、lint、相关格式检查通过;财务 SQLite、支付、插件 RPC 与产物专项通过。后端全量出现一项未改动的素材删除 SQLite 表锁失败,单独复跑 3 次通过,保留全量未通过记录。浏览器与真实商户验收待完成。#543 因迁移历史与能力限制回退暂缓。
2026-09-20 05:18:16 +08:00

158 lines
7.2 KiB
Go

package paymentplugins
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func TestEpayRequiresExplicitGateway(t *testing.T) {
if err := NewEpayProvider(nil).ValidateConfig(Config{"pid": "merchant", "key": "test-key"}); err == nil {
t.Fatal("missing gateway must not send merchant credentials to a default third party")
}
}
func TestEpayCheckoutURLAllowlist(t *testing.T) {
for _, value := range []string{"file://host/etc/passwd", "smb://host/share", "vscode://file/tmp/a", "custom://pay", "javascript://alert(1)", "data://host/value", "https://user:pass@pay.example", "weixin:", "//pay.example"} {
if isEpayURL(value) {
t.Errorf("accepted unsafe checkout: %s", value)
}
}
for _, value := range []string{"https://pay.example/order", "weixin://wxpay/bizpayurl?pr=test", "alipays://platformapi/startapp?appId=1", "mqqapi://wallet/pay"} {
if !isEpayURL(value) {
t.Errorf("rejected payment URL: %s", value)
}
}
}
func TestEpayQueryRejectsIncompleteEvidence(t *testing.T) {
for _, test := range []struct{ name, response string }{
{"missing order", `{"code":1,"status":1,"money":"1.00","trade_no":"trade"}`},
{"different order", `{"code":1,"out_trade_no":"other","status":1,"money":"1.00","trade_no":"trade"}`},
{"different merchant", `{"code":1,"pid":"other","out_trade_no":"order","status":1,"money":"1.00","trade_no":"trade"}`},
{"invalid amount", `{"code":1,"out_trade_no":"order","status":1,"money":"bad","trade_no":"trade"}`},
{"missing amount", `{"code":1,"out_trade_no":"order","status":1,"trade_no":"trade"}`},
{"zero amount", `{"code":1,"out_trade_no":"order","status":1,"money":"0","trade_no":"trade"}`},
{"missing transaction", `{"code":1,"out_trade_no":"order","status":1,"money":"1.00"}`},
{"missing status", `{"code":1,"out_trade_no":"order","money":"1.00","trade_no":"trade"}`},
{"trailing JSON", `{"code":1,"out_trade_no":"order","status":1,"money":"1.00","trade_no":"trade"}{}`},
} {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, test.response) }))
defer server.Close()
provider := NewEpayProvider(server.Client())
config := Config{"pid": "merchant", "key": "test-key", "gateway": server.URL}
if result, err := provider.QueryOrder(context.Background(), config, QueryRequest{MerchantOrderNo: "order"}); err == nil {
t.Fatalf("invalid payment evidence accepted: %+v", result)
}
if result, err := provider.CloseOrder(context.Background(), config, CloseRequest{MerchantOrderNo: "order"}); err == nil {
t.Fatalf("invalid evidence must not close an order: %+v", result)
}
})
}
}
func TestEpayQueryPreservesNumericTradeID(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"code":1,"out_trade_no":"order","status":1,"money":"1.00","trade_no":1234567890123456789}`)
}))
defer server.Close()
result, err := NewEpayProvider(server.Client()).QueryOrder(context.Background(), Config{"pid": "merchant", "key": "test-key", "gateway": server.URL}, QueryRequest{MerchantOrderNo: "order"})
if err != nil || !result.Paid || result.ProviderTradeNo != "1234567890123456789" {
t.Fatalf("trade ID lost precision: %+v, %v", result, err)
}
}
func TestEpayQRCodePreferenceFallsBackToRedirectForPayURL(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"code":1,"payurl":"https://pay.example/checkout"}`)
}))
defer server.Close()
checkout, err := NewEpayProvider(server.Client()).CreateOrder(context.Background(), Config{"pid": "merchant", "key": "test-key", "gateway": server.URL, "checkoutMode": "qr_code"}, CreateRequest{
MerchantOrderNo: "order", AmountFen: 100, Currency: "CNY", NotifyURL: "https://merchant.example/notify",
})
if err != nil || checkout.Mode != "redirect" {
t.Fatalf("checkout must match returned payload: %+v, %v", checkout, err)
}
}
func TestEpayNotificationRejectsDuplicateAndInvalidEvidence(t *testing.T) {
config := Config{"pid": "merchant", "key": "test-key", "gateway": "https://pay.example"}
for _, kind := range []string{"duplicate", "invalid amount", "zero amount", "missing transaction", "tampered", "pending", "closed", "missing status"} {
t.Run(kind, func(t *testing.T) {
values := map[string]string{"pid": "merchant", "out_trade_no": "order", "trade_no": "trade", "money": "1.00", "trade_status": "TRADE_SUCCESS"}
switch kind {
case "invalid amount":
values["money"] = "invalid"
case "zero amount":
values["money"] = "0"
case "missing transaction":
delete(values, "trade_no")
case "pending":
values["trade_status"] = "WAIT_BUYER_PAY"
case "closed":
values["trade_status"] = "TRADE_CLOSED"
case "missing status":
delete(values, "trade_status")
}
values["sign"] = epayMD5Sign(values, config["key"])
if kind == "tampered" {
values["money"] = "9.00"
}
form := url.Values{}
for key, value := range values {
form.Set(key, value)
}
if kind == "duplicate" {
form.Add("money", "9.00")
}
if _, err := NewEpayProvider(nil).VerifyNotification(context.Background(), config, nil, []byte(form.Encode())); err == nil {
t.Fatal("invalid notification accepted")
}
})
}
}
func TestEpayV2NotificationAndUnsupportedClose(t *testing.T) {
privateKey, _ := testRSAKeyPair(t)
config := Config{"pid": "merchant", "version": "1", "gateway": "https://pay.example", "privateKey": testPrivatePEM(privateKey), "platformPublicKey": testPublicPEM(&privateKey.PublicKey)}
values := map[string]string{"pid": "merchant", "out_trade_no": "order", "trade_no": "trade", "money": "1.00", "trade_status": "TRADE_SUCCESS", "sign_type": "RSA"}
signature, err := epayRSASign(values, config["privateKey"])
if err != nil {
t.Fatal(err)
}
values["sign"] = signature
form := url.Values{}
for key, value := range values {
form.Set(key, value)
}
provider := NewEpayProvider(nil)
notification, err := provider.VerifyNotification(context.Background(), config, nil, []byte(form.Encode()))
if err != nil || !notification.Paid || notification.AmountFen != 100 {
t.Fatalf("valid RSA notification rejected: %+v, %v", notification, err)
}
form.Set("money", "9.00")
if _, err := provider.VerifyNotification(context.Background(), config, nil, []byte(form.Encode())); err == nil {
t.Fatal("tampered RSA notification accepted")
}
if result, err := provider.CloseOrder(context.Background(), config, CloseRequest{MerchantOrderNo: "order"}); err == nil || result.Closed {
t.Fatalf("unsupported V2 close must not report success: %+v, %v", result, err)
}
if _, err := epayRSASign(values, "invalid-key"); err == nil {
t.Fatal("signing error must not be swallowed")
}
}
func TestEpayTransportErrorsDoNotExposeMerchantKey(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
server.Close()
_, err := NewEpayProvider(server.Client()).QueryOrder(context.Background(), Config{"pid": "merchant", "key": "secret-marker", "gateway": server.URL}, QueryRequest{MerchantOrderNo: "order"})
if err == nil || strings.Contains(err.Error(), "secret-marker") {
t.Fatalf("unsafe transport error: %v", err)
}
}