优化结构
This commit is contained in:
@@ -1,311 +0,0 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
"github.com/go-pay/gopay"
|
||||
"github.com/go-pay/gopay/saobei"
|
||||
)
|
||||
|
||||
type saobeiClientStub struct {
|
||||
miniPay func(context.Context, gopay.BodyMap) (*saobei.MiniPayRsp, error)
|
||||
barcodePay func(context.Context, gopay.BodyMap) (*saobei.BarcodePayRsp, error)
|
||||
query func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error)
|
||||
refund func(context.Context, gopay.BodyMap) (*saobei.RefundRsp, error)
|
||||
}
|
||||
|
||||
func (s *saobeiClientStub) MiniPay(ctx context.Context, bm gopay.BodyMap) (*saobei.MiniPayRsp, error) {
|
||||
return s.miniPay(ctx, bm)
|
||||
}
|
||||
|
||||
func (s *saobeiClientStub) BarcodePay(ctx context.Context, bm gopay.BodyMap) (*saobei.BarcodePayRsp, error) {
|
||||
return s.barcodePay(ctx, bm)
|
||||
}
|
||||
|
||||
func (s *saobeiClientStub) Query(ctx context.Context, bm gopay.BodyMap) (*saobei.QueryRsp, error) {
|
||||
return s.query(ctx, bm)
|
||||
}
|
||||
|
||||
func (s *saobeiClientStub) Refund(ctx context.Context, bm gopay.BodyMap) (*saobei.RefundRsp, error) {
|
||||
return s.refund(ctx, bm)
|
||||
}
|
||||
|
||||
func newSaobeiTestAdapter(client saobeiClient) *saobeiAdapter {
|
||||
return &saobeiAdapter{newClient: func(map[string]any) (saobeiClient, error) {
|
||||
return client, nil
|
||||
}}
|
||||
}
|
||||
|
||||
func TestSaobeiOKRequiresExplicitSuccess(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
returnCode string
|
||||
resultCode string
|
||||
want bool
|
||||
}{
|
||||
{name: "documented success", returnCode: "01", resultCode: "01", want: true},
|
||||
{name: "text success", returnCode: "SUCCESS", resultCode: "SUCCESS", want: true},
|
||||
{name: "missing result code", returnCode: "01", want: false},
|
||||
{name: "empty response"},
|
||||
{name: "business failure", returnCode: "01", resultCode: "02"},
|
||||
{name: "communication failure", returnCode: "02", resultCode: "01"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := saobeiOK(tc.returnCode, tc.resultCode); got != tc.want {
|
||||
t.Fatalf("saobeiOK(%q, %q) = %v, want %v", tc.returnCode, tc.resultCode, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiCreateKeepsQueryAndProviderTradeIDsSeparate(t *testing.T) {
|
||||
t.Run("mini pay", func(t *testing.T) {
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{miniPay: func(context.Context, gopay.BodyMap) (*saobei.MiniPayRsp, error) {
|
||||
return &saobei.MiniPayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, OutTradeNo: "SAOBEI-ORDER-1"}, nil
|
||||
}})
|
||||
result, err := adapter.Create(context.Background(), &biz.PaymentRequest{
|
||||
TradeNo: "LOCAL-1",
|
||||
Amount: 100,
|
||||
Extra: map[string]any{"sub_appid": "APP-1", "open_id": "OPEN-1"},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if result.TradeNo != "LOCAL-1" || result.QueryID != "SAOBEI-ORDER-1" || result.ProviderTradeNo != "" {
|
||||
t.Fatalf("Create() result = %+v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("barcode pay", func(t *testing.T) {
|
||||
var request gopay.BodyMap
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{barcodePay: func(_ context.Context, bm gopay.BodyMap) (*saobei.BarcodePayRsp, error) {
|
||||
request = bm
|
||||
return &saobei.BarcodePayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, OutTradeNo: "SAOBEI-ORDER-2", ChannelTradeNo: "CHANNEL-2"}, nil
|
||||
}})
|
||||
result, err := adapter.Create(context.Background(), &biz.PaymentRequest{
|
||||
TradeNo: "LOCAL-2",
|
||||
Amount: 200,
|
||||
Extra: map[string]any{"method": "barcode", "auth_code": "AUTH-2"},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if result.TradeNo != "LOCAL-2" || result.QueryID != "SAOBEI-ORDER-2" || result.ProviderTradeNo != "" {
|
||||
t.Fatalf("Create() result = %+v", result)
|
||||
}
|
||||
if request.GetString("auth_no") != "AUTH-2" {
|
||||
t.Fatalf("Create() auth_no = %q", request.GetString("auth_no"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSaobeiCreateRejectsMissingPlatformOrderID(t *testing.T) {
|
||||
for _, method := range []string{"", "barcode"} {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{
|
||||
miniPay: func(context.Context, gopay.BodyMap) (*saobei.MiniPayRsp, error) {
|
||||
return &saobei.MiniPayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}}, nil
|
||||
},
|
||||
barcodePay: func(context.Context, gopay.BodyMap) (*saobei.BarcodePayRsp, error) {
|
||||
return &saobei.BarcodePayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}}, nil
|
||||
},
|
||||
})
|
||||
extra := map[string]any{}
|
||||
if method != "" {
|
||||
extra["method"] = method
|
||||
}
|
||||
if _, err := adapter.Create(context.Background(), &biz.PaymentRequest{TradeNo: "LOCAL-MISSING", Amount: 1, Extra: extra}, nil); err == nil {
|
||||
t.Fatal("Create() accepted response without out_trade_no")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiCreateRejectsUnknownMethodBeforeClient(t *testing.T) {
|
||||
clientCreated := false
|
||||
adapter := &saobeiAdapter{newClient: func(map[string]any) (saobeiClient, error) {
|
||||
clientCreated = true
|
||||
return &saobeiClientStub{}, nil
|
||||
}}
|
||||
|
||||
_, err := adapter.Create(context.Background(), &biz.PaymentRequest{
|
||||
TradeNo: "LOCAL-UNKNOWN-METHOD", Amount: 1, Extra: map[string]any{"method": "unsupported"},
|
||||
}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("Create() accepted an unknown method")
|
||||
}
|
||||
if clientCreated {
|
||||
t.Fatal("Create() initialized the GoPay client before rejecting the unknown method")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiQueryRestoresMerchantTradeNoAndKeepsQueryID(t *testing.T) {
|
||||
var queryBody gopay.BodyMap
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(_ context.Context, bm gopay.BodyMap) (*saobei.QueryRsp, error) {
|
||||
queryBody = bm
|
||||
return &saobei.QueryRsp{
|
||||
RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"},
|
||||
PayTrace: "LOCAL-1",
|
||||
OutTradeNo: "SAOBEI-ORDER-1",
|
||||
ChannelTradeNo: "CHANNEL-1",
|
||||
TradeState: "SUCCESS",
|
||||
TotalFee: "100",
|
||||
BuyerPayFee: "90",
|
||||
}, nil
|
||||
}})
|
||||
|
||||
result, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", map[string]any{"currency": "cny"})
|
||||
if err != nil {
|
||||
t.Fatalf("Query() error = %v", err)
|
||||
}
|
||||
if queryBody.GetString("out_trade_no") != "SAOBEI-ORDER-1" {
|
||||
t.Fatalf("Query() out_trade_no = %q", queryBody.GetString("out_trade_no"))
|
||||
}
|
||||
if trace := queryBody.GetString("terminal_trace"); trace == "" || trace == "SAOBEI-ORDER-1" || len(trace) > 32 {
|
||||
t.Fatalf("Query() terminal_trace = %q, want an independent query流水号", trace)
|
||||
}
|
||||
if result.TradeNo != "LOCAL-1" || result.QueryID != "SAOBEI-ORDER-1" || result.ProviderTradeNo != "CHANNEL-1" {
|
||||
t.Fatalf("Query() result = %+v", result)
|
||||
}
|
||||
if result.Status != "success" || result.Amount != 100 || result.PayerPaidAmount != 90 || result.Currency != "CNY" {
|
||||
t.Fatalf("Query() payment fields = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiQueryRejectsMissingPaymentTrace(t *testing.T) {
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error) {
|
||||
return &saobei.QueryRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, OutTradeNo: "SAOBEI-ORDER-1", TradeState: "NOTPAY"}, nil
|
||||
}})
|
||||
|
||||
if _, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", nil); err == nil {
|
||||
t.Fatal("Query() accepted a response without pay_trace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiQueryRejectsInvalidTotalFee(t *testing.T) {
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error) {
|
||||
return &saobei.QueryRsp{
|
||||
RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"},
|
||||
PayTrace: "LOCAL-1",
|
||||
OutTradeNo: "SAOBEI-ORDER-1",
|
||||
TradeState: "SUCCESS",
|
||||
TotalFee: "invalid",
|
||||
BuyerPayFee: "90",
|
||||
}, nil
|
||||
}})
|
||||
|
||||
if _, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", nil); err == nil {
|
||||
t.Fatal("Query() accepted an invalid total_fee")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiQueryTreatsPayingAsPending(t *testing.T) {
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error) {
|
||||
return &saobei.QueryRsp{
|
||||
RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: saobei.ResultCodePaying},
|
||||
TerminalTrace: "LOCAL-QUERY-1",
|
||||
OutTradeNo: "SAOBEI-ORDER-1",
|
||||
TradeState: "SUCCESS",
|
||||
TotalFee: "50",
|
||||
ChannelTradeNo: "CHANNEL-1",
|
||||
}, nil
|
||||
}})
|
||||
|
||||
result, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Query() error = %v", err)
|
||||
}
|
||||
if result.Status != "pending" {
|
||||
t.Fatalf("Query() status = %q, want pending", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiRefundPrefersPersistedQueryID(t *testing.T) {
|
||||
var refundBody gopay.BodyMap
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(_ context.Context, bm gopay.BodyMap) (*saobei.RefundRsp, error) {
|
||||
refundBody = bm
|
||||
return &saobei.RefundRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, TerminalTrace: "LOCAL-REFUND-1", OutTradeNo: "SAOBEI-ORDER-1", OutRefundNo: "SAOBEI-REFUND-1", RefundFee: "50"}, nil
|
||||
}})
|
||||
|
||||
result, err := adapter.Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
TradeNo: "LOCAL-1", QueryID: "SAOBEI-ORDER-1", ProviderTradeNo: "CHANNEL-1",
|
||||
RefundNo: "LOCAL-REFUND-1", Amount: 50, Currency: "cny",
|
||||
}, map[string]any{"refund_extra": map[string]any{"out_refund_no": "MUST-NOT-BE-SENT"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Refund() error = %v", err)
|
||||
}
|
||||
if refundBody.GetString("out_trade_no") != "SAOBEI-ORDER-1" {
|
||||
t.Fatalf("Refund() out_trade_no = %q, want persisted QueryID", refundBody.GetString("out_trade_no"))
|
||||
}
|
||||
if refundBody.GetString("terminal_trace") != "LOCAL-REFUND-1" {
|
||||
t.Fatalf("Refund() terminal_trace = %q, want refund number", refundBody.GetString("terminal_trace"))
|
||||
}
|
||||
if result.TradeNo != "LOCAL-1" || result.ProviderTradeNo != "SAOBEI-REFUND-1" || result.Amount != 50 || result.Currency != "CNY" {
|
||||
t.Fatalf("Refund() result = %+v", result)
|
||||
}
|
||||
if _, ok := refundBody["out_refund_no"]; ok {
|
||||
t.Fatal("Refund() sent unsupported out_refund_no request field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiRefundFallsBackToMerchantTradeNo(t *testing.T) {
|
||||
var refundBody gopay.BodyMap
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(_ context.Context, bm gopay.BodyMap) (*saobei.RefundRsp, error) {
|
||||
refundBody = bm
|
||||
return &saobei.RefundRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, TerminalTrace: "LOCAL-REFUND-2", OutRefundNo: "SAOBEI-REFUND-2", RefundFee: "25"}, nil
|
||||
}})
|
||||
|
||||
_, err := adapter.Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
TradeNo: "LOCAL-2", RefundNo: "LOCAL-REFUND-2", Amount: 25, Currency: "CNY",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Refund() error = %v", err)
|
||||
}
|
||||
if refundBody.GetString("out_trade_no") != "LOCAL-2" {
|
||||
t.Fatalf("Refund() out_trade_no = %q, want merchant trade number fallback", refundBody.GetString("out_trade_no"))
|
||||
}
|
||||
if refundBody.GetString("terminal_trace") != "LOCAL-REFUND-2" {
|
||||
t.Fatalf("Refund() terminal_trace = %q, want refund number", refundBody.GetString("terminal_trace"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiRefundTreatsPayingAsPending(t *testing.T) {
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(context.Context, gopay.BodyMap) (*saobei.RefundRsp, error) {
|
||||
return &saobei.RefundRsp{
|
||||
RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: saobei.ResultCodePaying},
|
||||
TerminalTrace: "LOCAL-REFUND-3",
|
||||
OutRefundNo: "SAOBEI-REFUND-3",
|
||||
RefundFee: "25",
|
||||
}, nil
|
||||
}})
|
||||
|
||||
result, err := adapter.Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
TradeNo: "LOCAL-3", RefundNo: "LOCAL-REFUND-3", Amount: 25, Currency: "CNY",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Refund() error = %v", err)
|
||||
}
|
||||
if result.Status != "pending" || result.ProviderTradeNo != "SAOBEI-REFUND-3" {
|
||||
t.Fatalf("Refund() result = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaobeiRefundRejectsMismatchedAmount(t *testing.T) {
|
||||
adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(context.Context, gopay.BodyMap) (*saobei.RefundRsp, error) {
|
||||
return &saobei.RefundRsp{
|
||||
RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"},
|
||||
TerminalTrace: "LOCAL-REFUND-1",
|
||||
OutRefundNo: "SAOBEI-REFUND-1",
|
||||
RefundFee: "51",
|
||||
}, nil
|
||||
}})
|
||||
|
||||
if _, err := adapter.Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
TradeNo: "LOCAL-1", RefundNo: "LOCAL-REFUND-1", Amount: 50, Currency: "CNY",
|
||||
}, nil); err == nil {
|
||||
t.Fatal("Refund() accepted a mismatched refund amount")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user