kra-new/internal/integration/payment/wechat_v3_codepay_test.go

204 lines
7.4 KiB
Go

package payment
import (
"context"
"encoding/json"
"io"
bizpayment "kra/internal/biz/payment"
"net/http"
"net/http/httptest"
"strings"
"testing"
gopayWechatV3 "github.com/go-pay/gopay/wechat/v3"
)
func TestWechatV3CodePayUsesGoPaySDK(t *testing.T) {
platformKey, _, platformPublicPEM := testRSAKeyPair(t)
_, merchantPrivatePEM, _ := testRSAKeyPair(t)
const (
platformSerial = "PLATFORM-SERIAL"
timestamp = "1787068800"
nonce = "codepay-response-nonce"
)
response := []byte(`{"appid":"wx-test","mchid":"mch-test","out_trade_no":"T-WECHAT-CODEPAY","transaction_id":"WX-CODEPAY-1","trade_type":"MICROPAY","trade_state":"SUCCESS","amount":{"total":123,"payer_total":100,"currency":"CNY","payer_currency":"CNY"},"promotion_detail":[{"coupon_id":"COUPON-1","amount":23}]}`)
signature := testRSA2Sign(t, platformKey, []byte(timestamp+"\n"+nonce+"\n"+string(response)+"\n"))
var requestBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v3/pay/transactions/codepay" {
t.Errorf("request = %s %s", r.Method, r.URL.Path)
}
if strings.TrimSpace(r.Header.Get("Authorization")) == "" {
t.Error("missing GoPay authorization header")
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("read request: %v", err)
return
}
if err = json.Unmarshal(body, &requestBody); err != nil {
t.Errorf("decode request: %v", err)
return
}
w.Header().Set(gopayWechatV3.HeaderTimestamp, timestamp)
w.Header().Set(gopayWechatV3.HeaderNonce, nonce)
w.Header().Set(gopayWechatV3.HeaderSerial, platformSerial)
w.Header().Set(gopayWechatV3.HeaderSignature, signature)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response)
}))
defer server.Close()
result, err := (&wechatV3Adapter{}).Create(context.Background(), &bizpayment.PaymentRequest{
Provider: bizpayment.PaymentWechatV3,
TradeNo: "T-WECHAT-CODEPAY",
Subject: "subject",
Amount: 123,
Currency: "CNY",
NotifyURL: "https://merchant.test/payment/callback",
Extra: map[string]any{
"method": "codepay",
"auth_code": "AUTH-CODE-1",
"payer": map[string]any{"auth_code": "OVERRIDE"},
},
}, map[string]any{
"app_id": "wx-test",
"merchant_id": "mch-test",
"serial_no": "merchant-serial",
"private_key": merchantPrivatePEM,
"api_v3_key": "01234567890123456789012345678901",
"platform_cert": platformPublicPEM,
"platform_serial_no": platformSerial,
"create_url": server.URL,
})
if err != nil {
t.Fatal(err)
}
payer, _ := requestBody["payer"].(map[string]any)
amount, _ := requestBody["amount"].(map[string]any)
if payer["auth_code"] != "AUTH-CODE-1" || requestBody["out_trade_no"] != "T-WECHAT-CODEPAY" {
t.Fatalf("request identity = %#v", requestBody)
}
if amount["total"] != float64(123) || amount["currency"] != "CNY" {
t.Fatalf("request amount = %#v", amount)
}
if result.Provider != bizpayment.PaymentWechatV3 || result.Status != "success" || result.TradeNo != "T-WECHAT-CODEPAY" || result.ProviderTradeNo != "WX-CODEPAY-1" || result.QueryID != "" {
t.Fatalf("result identity = %+v", result)
}
if result.Amount != 123 || result.Currency != "CNY" || result.PayerCurrency != "CNY" || !result.AmountBreakdownKnown || result.PayerPaidAmount != 100 || result.CashPaidAmount != 100 || result.DiscountAmount != 23 {
t.Fatalf("result amount = %+v", result)
}
}
func TestWechatV3CodePayAliasesRequireAuthCode(t *testing.T) {
_, merchantPrivatePEM, _ := testRSAKeyPair(t)
_, _, platformPublicPEM := testRSAKeyPair(t)
config := map[string]any{
"app_id": "wx-test",
"merchant_id": "mch-test",
"serial_no": "merchant-serial",
"private_key": merchantPrivatePEM,
"api_v3_key": "01234567890123456789012345678901",
"platform_cert": platformPublicPEM,
"platform_serial_no": "platform-serial",
}
for _, method := range []string{"micropay", "micro_pay", "micro-pay", "codepay", "code_pay", "code-pay", "barcode", "barcode_pay", "facepay", "face_pay"} {
t.Run(method, func(t *testing.T) {
_, err := (&wechatV3Adapter{}).Create(context.Background(), &bizpayment.PaymentRequest{
TradeNo: "T-WECHAT-CODEPAY", Subject: "subject", Amount: 1, Currency: "CNY",
Extra: map[string]any{"method": method},
}, config)
if err == nil || !strings.Contains(err.Error(), "auth_code") {
t.Fatalf("method %q error = %v", method, err)
}
})
}
}
func TestWechatV3CodePayResultRejectsInvalidIdentity(t *testing.T) {
_, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{
Code: gopayWechatV3.Success,
Response: &gopayWechatV3.CodePay{
OutTradeNo: "OTHER-ORDER",
TradeState: "SUCCESS",
Amount: &gopayWechatV3.Amount{Total: 1, Currency: "CNY"},
},
})
if err == nil || !strings.Contains(err.Error(), "商户订单号不匹配") {
t.Fatalf("identity mismatch error = %v", err)
}
}
func TestWechatV3CodePayResultRequiresMerchantOrderNumber(t *testing.T) {
_, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{
Code: gopayWechatV3.Success,
Response: &gopayWechatV3.CodePay{
TradeState: "SUCCESS",
TransactionId: "WX-CODEPAY-1",
Amount: &gopayWechatV3.Amount{Total: 1, Currency: "CNY"},
},
})
if err == nil || !strings.Contains(err.Error(), "out_trade_no") {
t.Fatalf("missing out_trade_no error = %v", err)
}
}
func TestWechatV3CodePayResultPreservesFullyDiscountedBreakdown(t *testing.T) {
result, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{
Code: gopayWechatV3.Success,
Response: &gopayWechatV3.CodePay{
OutTradeNo: "T-WECHAT-CODEPAY",
TransactionId: "WX-CODEPAY-1",
TradeState: "SUCCESS",
Amount: &gopayWechatV3.Amount{
Total: 100, PayerTotal: 0, Currency: "CNY", PayerCurrency: "CNY",
},
},
})
if err != nil {
t.Fatal(err)
}
if !result.AmountBreakdownKnown || result.PayerPaidAmount != 0 || result.CashPaidAmount != 0 || result.DiscountAmount != 100 || result.PayerCurrency != "CNY" {
t.Fatalf("fully discounted result = %+v", result)
}
}
func TestWechatV3CodePayDoesNotInferMissingPayerTotal(t *testing.T) {
result, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{
Code: gopayWechatV3.Success,
SignInfo: &gopayWechatV3.SignInfo{SignBody: `{"out_trade_no":"T-WECHAT-CODEPAY","transaction_id":"WX-CODEPAY-2","trade_state":"SUCCESS","amount":{"total":100,"currency":"CNY"}}`},
Response: &gopayWechatV3.CodePay{
OutTradeNo: "T-WECHAT-CODEPAY",
TransactionId: "WX-CODEPAY-2",
TradeState: "SUCCESS",
Amount: &gopayWechatV3.Amount{Total: 100, Currency: "CNY"},
},
})
if err != nil {
t.Fatal(err)
}
if result.AmountBreakdownKnown || result.PayerPaidAmount != 0 || result.DiscountAmount != 0 {
t.Fatalf("missing payer_total was treated as a complete breakdown: %+v", result)
}
}
func TestWechatV3CodePayUserPayingRemainsPending(t *testing.T) {
result, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{
Code: gopayWechatV3.Success,
Response: &gopayWechatV3.CodePay{
OutTradeNo: "T-WECHAT-CODEPAY",
TradeState: "USERPAYING",
},
})
if err != nil {
t.Fatal(err)
}
if result.Status != "pending" || result.TradeNo != "T-WECHAT-CODEPAY" || result.ProviderTradeNo != "" || result.QueryID != "" {
t.Fatalf("pending result = %+v", result)
}
}