优化结构
This commit is contained in:
@@ -1,386 +0,0 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
"github.com/go-pay/gopay"
|
||||
"github.com/go-pay/gopay/allinpay"
|
||||
"github.com/go-pay/gopay/douyin"
|
||||
"github.com/go-pay/gopay/lakala"
|
||||
)
|
||||
|
||||
func paymentIdentityRSAKeyPair(t *testing.T) (*rsa.PrivateKey, string, string) {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
privatePEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||
publicDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicDER})
|
||||
return key, string(privatePEM), string(publicPEM)
|
||||
}
|
||||
|
||||
// GoPay clients use fixed provider hosts. Redirect their HTTPS transport to a
|
||||
// local server so adapter tests still exercise the SDK request and response path.
|
||||
func redirectPaymentHTTPS(t *testing.T, handler http.Handler) {
|
||||
t.Helper()
|
||||
server := httptest.NewTLSServer(handler)
|
||||
oldDefault := http.DefaultTransport
|
||||
base, ok := oldDefault.(*http.Transport)
|
||||
if !ok {
|
||||
server.Close()
|
||||
t.Fatal("http.DefaultTransport is not *http.Transport")
|
||||
}
|
||||
transport := base.Clone()
|
||||
transport.Proxy = nil
|
||||
transport.DisableKeepAlives = true
|
||||
transport.ForceAttemptHTTP2 = false
|
||||
transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: true} //nolint:gosec // local test server
|
||||
target := server.Listener.Addr().String()
|
||||
transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, target)
|
||||
}
|
||||
http.DefaultTransport = transport
|
||||
t.Cleanup(func() {
|
||||
http.DefaultTransport = oldDefault
|
||||
transport.CloseIdleConnections()
|
||||
server.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestLakalaKeepsQueryAndFinalTradeIdentitiesSeparate(t *testing.T) {
|
||||
created, err := lakalaCreateResult("MERCHANT-LAKALA-1", &lakala.PaymentRsp{
|
||||
ErrorCode: lakala.ErrorCode{ReturnCode: "SUCCESS", ResultCode: "SUCCESS"},
|
||||
OrderId: " LAKALA-ORDER-1 ",
|
||||
PartnerOrderId: "MERCHANT-LAKALA-1",
|
||||
CodeUrl: "https://pay.example/qr",
|
||||
SdkParams: `{"prepay":"client-only"}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if created.QueryID != "LAKALA-ORDER-1" || created.ProviderTradeNo != "" {
|
||||
t.Fatalf("create identities = query %q, provider trade %q", created.QueryID, created.ProviderTradeNo)
|
||||
}
|
||||
|
||||
redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if r.URL.Path != "/api/v1.0/gateway/partners/PART/orders/LAKALA-ORDER-1" {
|
||||
t.Errorf("query path = %q", r.URL.Path)
|
||||
}
|
||||
_, _ = fmt.Fprint(w, `{"return_code":"SUCCESS","result_code":"PAY_SUCCESS","order_id":"LAKALA-ORDER-1","partner_order_id":"MERCHANT-LAKALA-1","channel_order_id":"CHANNEL-LAKALA-1","total_fee":100,"real_fee":100,"currency":"JPY"}`)
|
||||
case http.MethodPut:
|
||||
if r.URL.Path != "/api/v1.0/gateway/partners/PART/orders/LAKALA-ORDER-1/refunds/REFUND-LAKALA-1" {
|
||||
t.Errorf("refund path = %q", r.URL.Path)
|
||||
}
|
||||
_, _ = fmt.Fprint(w, `{"return_code":"SUCCESS","result_code":"SUCCESS","refund_id":"LAKALA-REFUND-1","partner_refund_id":"REFUND-LAKALA-1","amount":40,"currency":"JPY"}`)
|
||||
default:
|
||||
t.Errorf("unexpected method %s", r.Method)
|
||||
http.Error(w, "unexpected method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}))
|
||||
|
||||
config := map[string]any{
|
||||
"partner_code": "PART", "credential_code": "credential",
|
||||
"order_id": "STATIC-CONFIG-MUST-NOT-BE-USED", "currency": "JPY",
|
||||
}
|
||||
result, err := (&lakalaAdapter{}).Query(context.Background(), created.QueryID, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.TradeNo != "MERCHANT-LAKALA-1" || result.QueryID != "LAKALA-ORDER-1" || result.ProviderTradeNo != "CHANNEL-LAKALA-1" {
|
||||
t.Fatalf("query identities = %+v", result)
|
||||
}
|
||||
if result.Status != "success" || result.Amount != 100 || result.Currency != "JPY" {
|
||||
t.Fatalf("query result = %+v", result)
|
||||
}
|
||||
|
||||
refund, err := (&lakalaAdapter{}).Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
TradeNo: "MERCHANT-LAKALA-1", QueryID: created.QueryID,
|
||||
RefundNo: "REFUND-LAKALA-1", Amount: 40, Currency: "JPY",
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if refund.TradeNo != "MERCHANT-LAKALA-1" || refund.ProviderTradeNo != "LAKALA-REFUND-1" {
|
||||
t.Fatalf("refund result = %+v", refund)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLakalaCreateRejectsMissingOrderID(t *testing.T) {
|
||||
_, err := lakalaCreateResult("MERCHANT-LAKALA-MISSING", &lakala.PaymentRsp{
|
||||
ErrorCode: lakala.ErrorCode{ReturnCode: "SUCCESS", ResultCode: "SUCCESS"},
|
||||
PartnerOrderId: "MERCHANT-LAKALA-MISSING",
|
||||
CodeUrl: "https://pay.example/qr",
|
||||
})
|
||||
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "order_id") {
|
||||
t.Fatalf("missing order_id error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLakalaQueryRejectsMissingMerchantOrderID(t *testing.T) {
|
||||
redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/api/v1.0/gateway/partners/PART/orders/LAKALA-ORDER-MISSING-MERCHANT" {
|
||||
t.Errorf("request = %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
_, _ = fmt.Fprint(w, `{"return_code":"SUCCESS","result_code":"PAY_SUCCESS","order_id":"LAKALA-ORDER-MISSING-MERCHANT","channel_order_id":"CHANNEL-LAKALA-1","total_fee":100,"currency":"JPY"}`)
|
||||
}))
|
||||
|
||||
_, err := (&lakalaAdapter{}).Query(context.Background(), "LAKALA-ORDER-MISSING-MERCHANT", map[string]any{
|
||||
"partner_code": "PART", "credential_code": "credential",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "商户订单号") {
|
||||
t.Fatalf("missing merchant order id error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLakalaCreateRoutesSupportedMethods(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
method string
|
||||
configMethod string
|
||||
tradeNo string
|
||||
path string
|
||||
appID string
|
||||
}
|
||||
cases := []testCase{
|
||||
{name: "default jsapi", tradeNo: "L-JSAPI-DEFAULT", path: "/api/v1.0/jsapi_gateway/partners/PART/orders/L-JSAPI-DEFAULT"},
|
||||
{name: "jsapi", method: "jsapi", tradeNo: "L-JSAPI", path: "/api/v1.0/jsapi_gateway/partners/PART/orders/L-JSAPI"},
|
||||
{name: "h5", method: "h5", tradeNo: "L-H5", path: "/api/v1.0/h5_payment/partners/PART/orders/L-H5"},
|
||||
{name: "mini", method: "mini", tradeNo: "L-MINI", path: "/api/v1.0/gateway/partners/PART/microapp_orders/L-MINI"},
|
||||
{name: "native", method: "native", tradeNo: "L-NATIVE", path: "/api/v1.0/gateway/partners/PART/native_orders/L-NATIVE"},
|
||||
{name: "qrcode", method: "qrcode", tradeNo: "L-QRCODE", path: "/api/v1.0/gateway/partners/PART/orders/L-QRCODE"},
|
||||
{name: "native jsapi", method: "native_jsapi", tradeNo: "L-NATIVE-JSAPI", path: "/api/v1.0/gateway/partners/PART/native_jsapi/L-NATIVE-JSAPI", appID: "wx-test-app"},
|
||||
{name: "sdk", method: "sdk", tradeNo: "L-SDK", path: "/api/v1.0/gateway/partners/PART/app_orders/L-SDK", appID: "wx-test-app"},
|
||||
{name: "web", method: "web", tradeNo: "L-WEB", path: "/api/v1.0/web_gateway/partners/PART/orders/L-WEB"},
|
||||
{name: "config method", configMethod: "h5", tradeNo: "L-CONFIG-H5", path: "/api/v1.0/h5_payment/partners/PART/orders/L-CONFIG-H5"},
|
||||
}
|
||||
expected := make(map[string]string, len(cases))
|
||||
for _, tc := range cases {
|
||||
expected[tc.path] = tc.tradeNo
|
||||
}
|
||||
redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("create method = %s, want PUT", r.Method)
|
||||
}
|
||||
tradeNo, ok := expected[r.URL.Path]
|
||||
if !ok {
|
||||
t.Errorf("unexpected Lakala create path = %q", r.URL.Path)
|
||||
http.Error(w, "unexpected path", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Errorf("decode create body: %v", err)
|
||||
}
|
||||
if body["description"] != "subject" || fmt.Sprint(body["price"]) != "100" || body["channel"] != "Wechat" {
|
||||
t.Errorf("create body = %#v", body)
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `{"return_code":"SUCCESS","result_code":"SUCCESS","order_id":"ORDER-%s","partner_order_id":"%s","code_url":"https://pay.example/%s","pay_url":"https://pay.example/pay/%s","sdk_params":"{\"prepay\":\"%s\"}"}`, tradeNo, tradeNo, tradeNo, tradeNo, tradeNo)
|
||||
}))
|
||||
|
||||
adapter := &lakalaAdapter{}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
extra := map[string]any{}
|
||||
if tc.method != "" {
|
||||
extra["method"] = tc.method
|
||||
}
|
||||
if tc.appID != "" {
|
||||
extra["appid"] = tc.appID
|
||||
}
|
||||
config := map[string]any{
|
||||
"partner_code": "PART", "credential_code": "credential", "channel": "Wechat", "currency": "JPY",
|
||||
}
|
||||
if tc.configMethod != "" {
|
||||
config["method"] = tc.configMethod
|
||||
}
|
||||
result, err := adapter.Create(context.Background(), &biz.PaymentRequest{
|
||||
Provider: biz.PaymentLakala, TradeNo: tc.tradeNo, Subject: "subject", Amount: 100,
|
||||
Currency: "JPY", Extra: extra,
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.QueryID != "ORDER-"+tc.tradeNo || result.ProviderTradeNo != "" {
|
||||
t.Fatalf("identities = %+v", result)
|
||||
}
|
||||
var payload lakala.PaymentRsp
|
||||
if err := json.Unmarshal(result.Payload, &payload); err != nil {
|
||||
t.Fatalf("decode payload: %v", err)
|
||||
}
|
||||
if payload.OrderId != "ORDER-"+tc.tradeNo || payload.PartnerOrderId != tc.tradeNo {
|
||||
t.Fatalf("payload = %+v", payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLakalaCreateRejectsUnknownMethod(t *testing.T) {
|
||||
_, err := (&lakalaAdapter{}).Create(context.Background(), &biz.PaymentRequest{
|
||||
Provider: biz.PaymentLakala, TradeNo: "L-UNKNOWN", Subject: "subject", Amount: 100,
|
||||
Currency: "JPY", Extra: map[string]any{"method": "unsupported"},
|
||||
}, map[string]any{"partner_code": "PART", "credential_code": "credential", "channel": "Wechat"})
|
||||
if err == nil || !strings.Contains(err.Error(), "不支持的下单方式") {
|
||||
t.Fatalf("unknown method error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllinPayPersistsQueryIDOnlyForTransactionLookupMode(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
config map[string]any
|
||||
want string
|
||||
}{
|
||||
{name: "default reqsn", config: map[string]any{}},
|
||||
{name: "explicit reqsn", config: map[string]any{"query_order_type": allinpay.OrderTypeReqSN}},
|
||||
{name: "trxid", config: map[string]any{"query_order_type": strings.ToUpper(allinpay.OrderTypeTrxId)}, want: "TRX-1"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
orderType, err := allinpayOrderType(tc.config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := allinpayCreateQueryID(orderType, " TRX-1 ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("query id = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllinPayQueryIdentityMatchesConfiguredLookupMode(t *testing.T) {
|
||||
key, _, _ := paymentIdentityRSAKeyPair(t)
|
||||
privateKey := base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(key))
|
||||
publicDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicKey := base64.StdEncoding.EncodeToString(publicDER)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
orderType string
|
||||
queryID string
|
||||
merchantNo string
|
||||
wantResultKey string
|
||||
}{
|
||||
{name: "merchant order number", queryID: "MERCHANT-ALLIN-1", merchantNo: "MERCHANT-ALLIN-1"},
|
||||
{name: "provider transaction number", orderType: allinpay.OrderTypeTrxId, queryID: "TRX-ALLIN-1", merchantNo: "MERCHANT-ALLIN-1", wantResultKey: "TRX-ALLIN-1"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/apiweb/tranx/query" {
|
||||
t.Errorf("request = %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
t.Errorf("parse form: %v", err)
|
||||
}
|
||||
if tc.orderType == allinpay.OrderTypeTrxId {
|
||||
if r.Form.Get("trxid") != tc.queryID || r.Form.Get("reqsn") != "" {
|
||||
t.Errorf("transaction query form = %v", r.Form)
|
||||
}
|
||||
} else if r.Form.Get("reqsn") != tc.queryID || r.Form.Get("trxid") != "" {
|
||||
t.Errorf("merchant query form = %v", r.Form)
|
||||
}
|
||||
response := signedAllinPayResponse(t, key, gopay.BodyMap{
|
||||
"retcode": "SUCCESS", "retmsg": "ok", "reqsn": tc.merchantNo,
|
||||
"trxid": "TRX-ALLIN-1", "trxstatus": "SUCCESS", "trxamt": "100",
|
||||
})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(response)
|
||||
}))
|
||||
|
||||
config := map[string]any{
|
||||
"cus_id": "customer", "app_id": "app", "private_key": privateKey,
|
||||
"public_key": publicKey, "environment": "sandbox", "currency": "CNY",
|
||||
}
|
||||
if tc.orderType != "" {
|
||||
config["query_order_type"] = tc.orderType
|
||||
}
|
||||
result, err := (&allinpayAdapter{}).Query(context.Background(), tc.queryID, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.TradeNo != tc.merchantNo || result.ProviderTradeNo != "TRX-ALLIN-1" || result.QueryID != tc.wantResultKey {
|
||||
t.Fatalf("query identities = %+v", result)
|
||||
}
|
||||
if result.Status != "success" || result.Amount != 100 || result.Currency != "CNY" {
|
||||
t.Fatalf("query result = %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDouyinPrepayPayloadSignaturesAreVerifiable(t *testing.T) {
|
||||
key, privatePEM, _ := paymentIdentityRSAKeyPair(t)
|
||||
client, err := douyin.NewClient("merchant-douyin", "merchant-serial", "01234567890123456789012345678901", privatePEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, app := range []bool{false, true} {
|
||||
result, err := douyinPrepayResult(client, "douyin-app", "MERCHANT-DOUYIN-1", "PREPAY-DOUYIN-1", app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.QueryID != "" || result.ProviderTradeNo != "" {
|
||||
t.Fatalf("prepay id leaked into durable identity: %+v", result)
|
||||
}
|
||||
|
||||
var content, signature string
|
||||
if app {
|
||||
var params douyin.AppPayParams
|
||||
if err = json.Unmarshal(result.Payload, ¶ms); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if params.AppId != "douyin-app" || params.PartnerId != "merchant-douyin" || params.PrepayId != "PREPAY-DOUYIN-1" {
|
||||
t.Fatalf("app params = %+v", params)
|
||||
}
|
||||
content = params.AppId + "\n" + params.Timestamp + "\n" + params.NonceStr + "\n" + params.PrepayId + "\n"
|
||||
signature = params.Sign
|
||||
} else {
|
||||
var params douyin.JSAPIPayParams
|
||||
if err = json.Unmarshal(result.Payload, ¶ms); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if params.AppId != "douyin-app" || params.Package != "prepay_id=PREPAY-DOUYIN-1" || params.SignType != douyin.SignTypeRSA {
|
||||
t.Fatalf("JSAPI params = %+v", params)
|
||||
}
|
||||
content = params.AppId + "\n" + params.TimeStamp + "\n" + params.NonceStr + "\n" + params.Package + "\n"
|
||||
signature = params.PaySign
|
||||
}
|
||||
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256([]byte(content))
|
||||
if err = rsa.VerifyPKCS1v15(&key.PublicKey, crypto.SHA256, digest[:], signatureBytes); err != nil {
|
||||
t.Fatalf("app=%v signature verification: %v", app, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user