Files
ttstd-infinite-canvas/backend/payment-plugins/xunhupay_test.go
ddCat bb2a79d365 feat(payment): 虎皮椒聚合支付 - 官方插件与跨平台可执行文件
新增 xunhupay 官方包,支付 RPC 与宿主只保留订单和验签边界;官方支付包同时带上各系统 provider,避免把 Linux ELF 当成本机入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 23:32:11 +08:00

169 lines
6.5 KiB
Go

package paymentplugins
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestXunHuPayCreateOrderSignsJSONAndReturnsQRCode(t *testing.T) {
fixedNow := time.Date(2026, 9, 17, 12, 0, 0, 0, time.FixedZone("CST", 8*60*60))
config := testXunHuPayConfig()
var captured map[string]string
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost || request.URL.Path != "/payment/do.html" {
t.Fatalf("request = %s %s", request.Method, request.URL.Path)
}
if !strings.Contains(request.Header.Get("Content-Type"), "application/json") {
t.Fatalf("content-type = %q", request.Header.Get("Content-Type"))
}
body, err := io.ReadAll(request.Body)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(body, &captured); err != nil {
t.Fatal(err)
}
if captured["trade_order_id"] != "order-1" || captured["total_fee"] != "1.00" || captured["version"] != "1.1" {
t.Fatalf("payload = %#v", captured)
}
if captured["hash"] != xunhupayHash(captured, config["appSecret"]) {
t.Fatalf("hash = %q", captured["hash"])
}
response := map[string]string{
"errcode": "0", "errmsg": "success!", "url_qrcode": "https://api.xunhupay.com/qrcode.png",
"url": "https://api.xunhupay.com/pay/jump",
}
response["hash"] = xunhupayHash(response, config["appSecret"])
_ = json.NewEncoder(writer).Encode(response)
}))
defer server.Close()
provider := NewXunHuPayProvider(server.Client())
provider.baseURL = server.URL
provider.now = func() time.Time { return fixedNow }
provider.nonce = func() string { return "fixed-nonce" }
checkout, err := provider.CreateOrder(context.Background(), config, CreateRequest{
MerchantOrderNo: "order-1", Description: "100 积分", AmountFen: 100, Currency: "CNY",
ExpiresAt: fixedNow.Add(30 * time.Minute), NotifyURL: "https://merchant.example/api/payments/notify/xunhupay-aggregate/cfg",
ReturnURL: "https://merchant.example/api/payments/return/xunhupay-aggregate?orderId=1",
})
if err != nil {
t.Fatal(err)
}
if checkout.Mode != "qr_code" || checkout.Value != "https://api.xunhupay.com/qrcode.png" {
t.Fatalf("checkout = %#v", checkout)
}
if !checkout.ExpiresAt.Equal(fixedNow.Add(5 * time.Minute)) {
t.Fatalf("expiresAt = %s", checkout.ExpiresAt)
}
}
func TestXunHuPayCreateOrderCanReturnRedirectCheckout(t *testing.T) {
config := testXunHuPayConfig()
config["checkoutMode"] = "redirect"
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
response := map[string]string{"errcode": "0", "errmsg": "success!", "url": "https://api.xunhupay.com/pay/jump"}
response["hash"] = xunhupayHash(response, config["appSecret"])
_ = json.NewEncoder(writer).Encode(response)
}))
defer server.Close()
provider := NewXunHuPayProvider(server.Client())
provider.baseURL = server.URL
provider.nonce = func() string { return "fixed-nonce" }
checkout, err := provider.CreateOrder(context.Background(), config, CreateRequest{
MerchantOrderNo: "order-2", Description: "充值", AmountFen: 200, Currency: "CNY",
ExpiresAt: time.Now().Add(20 * time.Minute), NotifyURL: "https://merchant.example/notify",
})
if err != nil {
t.Fatal(err)
}
if checkout.Mode != "redirect" || checkout.Value != "https://api.xunhupay.com/pay/jump" {
t.Fatalf("checkout = %#v", checkout)
}
}
func TestXunHuPayQueryAndCloseMapOfficialStatuses(t *testing.T) {
config := testXunHuPayConfig()
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/payment/query.html" {
t.Fatalf("path = %s", request.URL.Path)
}
var payload map[string]string
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload["out_trade_order"] != "order-3" || payload["hash"] != xunhupayHash(payload, config["appSecret"]) {
t.Fatalf("payload = %#v", payload)
}
response := map[string]any{
"errcode": 0, "errmsg": "success!",
"data": map[string]string{"status": "WP", "out_trade_order": "order-3", "open_order_id": "open-3", "total_fee": "1.50"},
}
_ = json.NewEncoder(writer).Encode(response)
}))
defer server.Close()
provider := NewXunHuPayProvider(server.Client())
provider.baseURL = server.URL
provider.nonce = func() string { return "fixed-nonce" }
result, err := provider.QueryOrder(context.Background(), config, QueryRequest{MerchantOrderNo: "order-3"})
if err != nil {
t.Fatal(err)
}
if result.Paid || result.Closed || result.AmountFen != 150 || result.ProviderStatus != "WP" {
t.Fatalf("query = %#v", result)
}
closed, err := provider.CloseOrder(context.Background(), config, CloseRequest{MerchantOrderNo: "order-3"})
if err != nil {
t.Fatal(err)
}
if !closed.Closed || closed.Paid || closed.ProviderStatus != "CD" {
t.Fatalf("close = %#v", closed)
}
}
func TestXunHuPayNotificationVerifiesFormHash(t *testing.T) {
config := testXunHuPayConfig()
values := map[string]string{
"trade_order_id": "order-4", "total_fee": "2.00", "transaction_id": "tx-4",
"open_order_id": "open-4", "status": "OD", "appid": config["appId"], "time": "1788321600", "nonce_str": "cb-nonce",
}
values["hash"] = xunhupayHash(values, config["appSecret"])
body := "trade_order_id=order-4&total_fee=2.00&transaction_id=tx-4&open_order_id=open-4&status=OD&appid=" + config["appId"] + "&time=1788321600&nonce_str=cb-nonce&hash=" + values["hash"]
notification, err := NewXunHuPayProvider(http.DefaultClient).VerifyNotification(context.Background(), config, nil, []byte(body))
if err != nil {
t.Fatal(err)
}
if !notification.Paid || notification.AmountFen != 200 || notification.MerchantOrderNo != "order-4" || notification.ProviderTradeNo != "tx-4" {
t.Fatalf("notification = %#v", notification)
}
}
func TestXunHuPayRejectsUnknownGatewayAndMissingBills(t *testing.T) {
provider := NewXunHuPayProvider(http.DefaultClient)
config := testXunHuPayConfig()
config["gateway"] = "https://evil.example/payment/do.html"
if err := provider.ValidateConfig(config); err == nil {
t.Fatal("expected untrusted gateway to fail")
}
_, err := provider.DownloadTradeBill(context.Background(), testXunHuPayConfig(), time.Now())
if !errors.Is(err, ErrTradeBillNotFound) {
t.Fatalf("bill error = %v", err)
}
}
func testXunHuPayConfig() Config {
return Config{
"publicBaseUrl": "https://merchant.example",
"appId": "20146122002",
"appSecret": "test-app-secret",
"gateway": defaultXunHuPayGateway,
}
}