优化结构
This commit is contained in:
@@ -1,333 +0,0 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
gopayAlipay "github.com/go-pay/gopay/alipay"
|
||||
gopayAlipayV3 "github.com/go-pay/gopay/alipay/v3"
|
||||
)
|
||||
|
||||
func testAlipayV3Config(t *testing.T) (*rsa.PrivateKey, map[string]any) {
|
||||
t.Helper()
|
||||
key, privatePEM, _ := testRSAKeyPair(t)
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1001),
|
||||
Subject: pkixName("GoPay Alipay V3 Test"),
|
||||
Issuer: pkixName("GoPay Alipay V3 Test"),
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificate := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
|
||||
return key, map[string]any{
|
||||
"app_id": "app-v3-test",
|
||||
"private_key": privatePEM,
|
||||
"app_cert": certificate,
|
||||
"root_cert": certificate,
|
||||
"public_cert": certificate,
|
||||
"environment": "sandbox",
|
||||
}
|
||||
}
|
||||
|
||||
func pkixName(commonName string) pkix.Name {
|
||||
return pkix.Name{CommonName: commonName, Organization: []string{"GoPay Tests"}}
|
||||
}
|
||||
|
||||
func cloneAlipayV3Config(values map[string]any) map[string]any {
|
||||
clone := make(map[string]any, len(values)+1)
|
||||
for key, value := range values {
|
||||
clone[key] = value
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func alipayV3ResponseSignature(t *testing.T, key *rsa.PrivateKey, body string) (string, string, string) {
|
||||
t.Helper()
|
||||
timestamp := "1660000000000"
|
||||
nonce := "alipay-v3-test-nonce"
|
||||
signature := testRSA2Sign(t, key, []byte(timestamp+"\n"+nonce+"\n"+body+"\n"))
|
||||
return timestamp, nonce, signature
|
||||
}
|
||||
|
||||
func writeAlipayV3Response(w http.ResponseWriter, timestamp, nonce, signature, body string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set(gopayAlipayV3.HeaderTimestamp, timestamp)
|
||||
w.Header().Set(gopayAlipayV3.HeaderNonce, nonce)
|
||||
w.Header().Set(gopayAlipayV3.HeaderSignature, signature)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = io.WriteString(w, body)
|
||||
}
|
||||
|
||||
func TestAlipayV3CreateRESTMethodsUseGoPay(t *testing.T) {
|
||||
platformKey, baseConfig := testAlipayV3Config(t)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
response string
|
||||
extra map[string]any
|
||||
wantStatus string
|
||||
wantTrade string
|
||||
}{
|
||||
{
|
||||
name: "trade create", path: "/v3/alipay/trade/create",
|
||||
response: `{"trade_no":"P-V3-CREATE","out_trade_no":"T-V3-CREATE"}`,
|
||||
extra: map[string]any{"buyer_open_id": "buyer-open-id"}, wantStatus: "created", wantTrade: "P-V3-CREATE",
|
||||
},
|
||||
{
|
||||
name: "trade pay", method: "barcode", path: "/v3/alipay/trade/pay",
|
||||
response: `{"trade_no":"P-V3-PAY","out_trade_no":"T-V3-PAY","total_amount":"10.00","buyer_pay_amount":"9.00","point_amount":"1.00","receipt_amount":"9.00"}`,
|
||||
extra: map[string]any{"auth_code": "BARCODE-V3"}, wantStatus: "success", wantTrade: "P-V3-PAY",
|
||||
},
|
||||
{
|
||||
name: "precreate", method: "native", path: "/v3/alipay/trade/precreate",
|
||||
response: `{"out_trade_no":"T-V3-PRECREATE","qr_code":"https://qr.example/v3"}`,
|
||||
wantStatus: "created",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tradeNo := "T-V3-CREATE"
|
||||
if tc.method == "barcode" {
|
||||
tradeNo = "T-V3-PAY"
|
||||
} else if tc.method == "native" {
|
||||
tradeNo = "T-V3-PRECREATE"
|
||||
}
|
||||
timestamp, nonce, signature := alipayV3ResponseSignature(t, platformKey, tc.response)
|
||||
var requestBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != tc.path {
|
||||
t.Errorf("request path = %q, want %q", r.URL.Path, tc.path)
|
||||
}
|
||||
if !strings.HasPrefix(r.Header.Get(gopayAlipayV3.HeaderAuthorization), gopayAlipayV3.SignTypeRSA+" ") {
|
||||
t.Errorf("authorization = %q", r.Header.Get(gopayAlipayV3.HeaderAuthorization))
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Errorf("decode request: %v", err)
|
||||
}
|
||||
writeAlipayV3Response(w, timestamp, nonce, signature, tc.response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := cloneAlipayV3Config(baseConfig)
|
||||
config["api_base_url"] = server.URL
|
||||
extra := cloneAlipayV3Config(tc.extra)
|
||||
if tc.method != "" {
|
||||
extra["method"] = tc.method
|
||||
}
|
||||
result, err := (&alipayV3Adapter{}).Create(context.Background(), &biz.PaymentRequest{
|
||||
Provider: biz.PaymentAlipayV3, TradeNo: tradeNo, Subject: "subject", Amount: 1000,
|
||||
Currency: "CNY", NotifyURL: "https://merchant.example/alipay-v3/notify", Extra: extra,
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if requestBody == nil || requestBody["out_trade_no"] != tradeNo || requestBody["notify_url"] == "" {
|
||||
t.Fatalf("request body = %#v", requestBody)
|
||||
}
|
||||
if result.Provider != biz.PaymentAlipayV3 || result.Status != tc.wantStatus || result.TradeNo != tradeNo || result.ProviderTradeNo != tc.wantTrade {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
if tc.method == "barcode" {
|
||||
if requestBody["scene"] != "bar_code" || requestBody["auth_code"] != "BARCODE-V3" {
|
||||
t.Fatalf("trade pay request = %#v", requestBody)
|
||||
}
|
||||
if !result.AmountBreakdownKnown || result.Amount != 1000 || result.PayerPaidAmount != 900 || result.PointPaidAmount != 100 {
|
||||
t.Fatalf("trade pay amount breakdown = %+v", result)
|
||||
}
|
||||
}
|
||||
if tc.method == "" && (requestBody["product_code"] != "JSAPI_PAY" || requestBody["op_app_id"] != "app-v3-test") {
|
||||
t.Fatalf("trade create defaults = %#v", requestBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlipayV3GeneratedPaymentMethodsUseGoPay(t *testing.T) {
|
||||
_, config := testAlipayV3Config(t)
|
||||
config["gateway_url"] = "https://merchant-gateway.example/gateway.do"
|
||||
for _, method := range []string{"app", "page", "wap"} {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
result, err := (&alipayV3Adapter{}).Create(context.Background(), &biz.PaymentRequest{
|
||||
Provider: biz.PaymentAlipayV3, TradeNo: "T-V3-" + strings.ToUpper(method), Subject: "subject",
|
||||
Amount: 1000, Currency: "CNY", NotifyURL: "https://merchant.example/alipay-v3/notify",
|
||||
ReturnURL: "https://merchant.example/alipay-v3/return", Extra: map[string]any{"method": method},
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var payload map[string]string
|
||||
if err = json.Unmarshal(result.Payload, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
generated := payload["order_string"]
|
||||
if generated == "" {
|
||||
generated = payload["pay_url"]
|
||||
}
|
||||
if !strings.Contains(generated, "alipay.trade."+method+".pay") || !strings.Contains(generated, "notify_url=") {
|
||||
t.Fatalf("generated payment data = %q", generated)
|
||||
}
|
||||
if method != "app" && !strings.HasPrefix(generated, "https://merchant-gateway.example/gateway.do?") {
|
||||
t.Fatalf("rewritten gateway URL = %q", generated)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlipayV3QueryVerifiesSignedIdentityAndAmount(t *testing.T) {
|
||||
platformKey, config := testAlipayV3Config(t)
|
||||
body := `{"trade_no":"P-V3-QUERY","out_trade_no":"T-V3-QUERY","trade_status":"TRADE_SUCCESS","total_amount":"10.00","buyer_pay_amount":"9.00","point_amount":"1.00","receipt_amount":"9.00"}`
|
||||
timestamp, nonce, signature := alipayV3ResponseSignature(t, platformKey, body)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v3/alipay/trade/query" {
|
||||
t.Errorf("request path = %q", r.URL.Path)
|
||||
}
|
||||
writeAlipayV3Response(w, timestamp, nonce, signature, body)
|
||||
}))
|
||||
defer server.Close()
|
||||
config["api_base_url"] = server.URL
|
||||
|
||||
result, err := (&alipayV3Adapter{}).Query(context.Background(), "T-V3-QUERY", config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Status != "success" || result.TradeNo != "T-V3-QUERY" || result.ProviderTradeNo != "P-V3-QUERY" || result.Amount != 1000 || result.Currency != "CNY" {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
if !result.AmountBreakdownKnown || result.PayerPaidAmount != 900 || result.CashPaidAmount != 800 || result.PointPaidAmount != 100 || result.DiscountAmount != 100 || result.SettlementAmount != 900 {
|
||||
t.Fatalf("amount breakdown = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlipayV3QueryRejectsInvalidSignatureAndIdentity(t *testing.T) {
|
||||
platformKey, baseConfig := testAlipayV3Config(t)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
body string
|
||||
signature string
|
||||
want string
|
||||
}{
|
||||
{name: "mismatched order", body: `{"trade_no":"P-V3","out_trade_no":"OTHER","trade_status":"TRADE_SUCCESS","total_amount":"10.00"}`, want: "out_trade_no"},
|
||||
{name: "invalid signature", body: `{"trade_no":"P-V3","out_trade_no":"T-V3-QUERY","trade_status":"TRADE_SUCCESS","total_amount":"10.00"}`, signature: "invalid", want: "signature"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
timestamp, nonce, signature := alipayV3ResponseSignature(t, platformKey, tc.body)
|
||||
if tc.signature != "" {
|
||||
signature = tc.signature
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeAlipayV3Response(w, timestamp, nonce, signature, tc.body)
|
||||
}))
|
||||
defer server.Close()
|
||||
config := cloneAlipayV3Config(baseConfig)
|
||||
config["api_base_url"] = server.URL
|
||||
_, err := (&alipayV3Adapter{}).Query(context.Background(), "T-V3-QUERY", config)
|
||||
if err == nil || !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tc.want)) {
|
||||
t.Fatalf("error = %v, want %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlipayV3RefundValidatesResponse(t *testing.T) {
|
||||
platformKey, baseConfig := testAlipayV3Config(t)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
refundFee string
|
||||
wantError bool
|
||||
}{
|
||||
{name: "success", refundFee: "1.23"},
|
||||
{name: "amount mismatch", refundFee: "1.22", wantError: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
body := `{"trade_no":"P-V3-REFUND","out_trade_no":"T-V3-REFUND","fund_change":"Y","refund_fee":"` + tc.refundFee + `"}`
|
||||
timestamp, nonce, signature := alipayV3ResponseSignature(t, platformKey, body)
|
||||
var requestBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Errorf("decode request: %v", err)
|
||||
}
|
||||
writeAlipayV3Response(w, timestamp, nonce, signature, body)
|
||||
}))
|
||||
defer server.Close()
|
||||
config := cloneAlipayV3Config(baseConfig)
|
||||
config["api_base_url"] = server.URL
|
||||
result, err := (&alipayV3Adapter{}).Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
Provider: biz.PaymentAlipayV3, TradeNo: "T-V3-REFUND", ProviderTradeNo: "P-V3-REFUND",
|
||||
RefundNo: "R-V3-REFUND", Amount: 123, Currency: "CNY",
|
||||
}, config)
|
||||
if tc.wantError {
|
||||
if err == nil || !strings.Contains(err.Error(), "金额不匹配") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if requestBody["out_request_no"] != "R-V3-REFUND" || requestBody["refund_amount"] != "1.23" {
|
||||
t.Fatalf("refund request = %#v", requestBody)
|
||||
}
|
||||
if result.Amount != 123 || result.ProviderTradeNo != "P-V3-REFUND" || result.Status != "created" {
|
||||
t.Fatalf("refund result = %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlipayV3CallbackUsesGoPayCertificateVerification(t *testing.T) {
|
||||
platformKey, config := testAlipayV3Config(t)
|
||||
fields := map[string]string{
|
||||
"app_id": "app-v3-test",
|
||||
"trade_no": "P-V3-CALLBACK",
|
||||
"out_trade_no": "T-V3-CALLBACK",
|
||||
"trade_status": "TRADE_SUCCESS",
|
||||
"sign_type": "RSA2",
|
||||
}
|
||||
bodyMap := toGoPayBodyMap(fields)
|
||||
bodyMap.Remove("sign_type")
|
||||
signature, err := gopayAlipay.GetRsaSign(bodyMap, gopayAlipay.RSA2, platformKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fields["sign"] = signature
|
||||
values := url.Values{}
|
||||
for key, value := range fields {
|
||||
values.Set(key, value)
|
||||
}
|
||||
callback := &biz.PaymentCallback{
|
||||
Body: []byte(values.Encode()), Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}
|
||||
result, err := (&alipayV3Adapter{}).Callback(context.Background(), callback, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Provider != biz.PaymentAlipayV3 || result.Status != "success" || result.TradeNo != "T-V3-CALLBACK" || result.ProviderTradeNo != "P-V3-CALLBACK" {
|
||||
t.Fatalf("callback result = %+v", result)
|
||||
}
|
||||
callback.Body = []byte(strings.ReplaceAll(values.Encode(), "P-V3-CALLBACK", "P-V3-TAMPERED"))
|
||||
if _, err = (&alipayV3Adapter{}).Callback(context.Background(), callback, config); err == nil {
|
||||
t.Fatal("tampered callback unexpectedly verified")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user