Files
hfb_jysh/backend/internal/integrations/payment/saobei/client_test.go
T

248 lines
7.5 KiB
Go

package saobei
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestSignIncludesEmptyFieldsAndUsesAccessToken(t *testing.T) {
params := map[string]string{
"merchant_no": "M1",
"terminal_id": "T1",
"amount": "100",
"attach": "",
"key_sign": "ignored",
}
base := SignBaseString(params)
wantBase := "amount=100&attach=&merchant_no=M1&terminal_id=T1"
if base != wantBase {
t.Fatalf("SignBaseString() = %q, want %q", base, wantBase)
}
if got, want := Sign(params, "secret"), "7258c86e8a1e4a2f1ae66c2855f46d24"; got != want {
t.Fatalf("Sign() = %q, want %q", got, want)
}
}
func TestCreatePaymentPostsQRPayAndVerifiesResponse(t *testing.T) {
var captured map[string]string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != endpointQRPay {
t.Fatalf("path = %q, want %q", r.URL.Path, endpointQRPay)
}
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
t.Fatalf("decode request: %v", err)
}
if captured["service_id"] != "016" || captured["pay_type"] != "000" {
t.Fatalf("unexpected qrpay request: %#v", captured)
}
if captured["key_sign"] != Sign(captured, "token-1") {
t.Fatalf("request key_sign = %q", captured["key_sign"])
}
response := map[string]string{
"return_code": "01",
"return_msg": "预支付请求成功",
"result_code": "01",
"merchant_no": "M1",
"terminal_id": "T1",
"terminal_trace": "PAY1",
"terminal_time": captured["terminal_time"],
"total_fee": "100",
"qr_url": "https://pay.example/qr1",
}
response["key_sign"] = Sign(response, "token-1")
_ = json.NewEncoder(w).Encode(response)
}))
defer server.Close()
client := NewClient(Config{
GatewayURL: server.URL,
MerchantID: "M1",
TerminalID: "T1",
AccessToken: "token-1",
NotifyURL: "https://example.com/api/payments/saobei/notify",
})
resp, err := client.CreatePayment(context.Background(), CreatePaymentRequest{
ThirdOrderID: "PAY1",
AmountCent: 100,
Body: "租号订单 ORD1",
Attach: "ORD1",
})
if err != nil {
t.Fatalf("CreatePayment() error = %v", err)
}
if !resp.OK || resp.QRURL != "https://pay.example/qr1" || resp.Status != "paying" {
t.Fatalf("CreatePayment() response = %+v", resp)
}
if captured["notify_url"] != "https://example.com/api/payments/saobei/notify" {
t.Fatalf("notify_url = %q", captured["notify_url"])
}
if captured["repeated_trace"] != "3" || captured["auto_pay"] != "1" {
t.Fatalf("qrpay safety fields = %#v", captured)
}
if captured["order_body"] != "租号订单-ORD1" {
t.Fatalf("order_body = %q, want safe characters only", captured["order_body"])
}
}
func TestCreatePaymentTreatsUnsignedFailureAsBusinessError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"return_code":"02","return_msg":"签名未通过校验!","result_code":"02"}`))
}))
defer server.Close()
client := NewClient(Config{
GatewayURL: server.URL,
MerchantID: "M1",
TerminalID: "T1",
AccessToken: "token-1",
})
resp, err := client.CreatePayment(context.Background(), CreatePaymentRequest{
ThirdOrderID: "PAY1",
AmountCent: 100,
})
if err != nil {
t.Fatalf("CreatePayment() error = %v, want nil business error", err)
}
if resp.OK || resp.ErrorMessage == "" || resp.Raw["return_code"] != "02" {
t.Fatalf("CreatePayment() response = %+v", resp)
}
}
func TestQueryPaymentUsesTraceTimeAndMapsSuccess(t *testing.T) {
var captured map[string]string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
t.Fatalf("decode query: %v", err)
}
response := map[string]string{
"return_code": "01",
"return_msg": "支付成功",
"result_code": "01",
"merchant_no": "M1",
"pay_trace": "PAY1",
"pay_time": "20261111120000",
"out_trade_no": "SB1",
"total_fee": "100",
"trade_state": "SUCCESS",
"end_time": "20261111120100",
"terminal_time": captured["terminal_time"],
}
response["key_sign"] = Sign(response, "token-1")
_ = json.NewEncoder(w).Encode(response)
}))
defer server.Close()
client := NewClient(Config{
GatewayURL: server.URL,
MerchantID: "M1",
TerminalID: "T1",
AccessToken: "token-1",
})
resp, err := client.QueryPayment(context.Background(), QueryPaymentRequest{
ThirdOrderID: "PAY1",
TerminalTime: "20261111120000",
})
if err != nil {
t.Fatalf("QueryPayment() error = %v", err)
}
if captured["pay_trace"] != "PAY1" || captured["pay_time"] != "20261111120000" {
t.Fatalf("query trace fields = %#v", captured)
}
if resp.Status != "paid" || resp.ProviderOrderID != "SB1" || resp.Amount != "100" {
t.Fatalf("QueryPayment() response = %+v", resp)
}
}
func TestParseNotifyAndVerifySignature(t *testing.T) {
params := map[string]string{
"pay_ver": "202",
"return_code": "01",
"return_msg": "支付成功",
"result_code": "01",
"merchant_no": "M1",
"terminal_id": "T1",
"terminal_trace": "PAY1",
"terminal_time": "20261111120000",
"end_time": "20261111120100",
"out_trade_no": "SB1",
"total_fee": "100",
}
params["key_sign"] = Sign(params, "token-1")
body, _ := json.Marshal(params)
parsed, err := ParsePayload(body)
if err != nil {
t.Fatalf("ParsePayload() error = %v", err)
}
canonical := CanonicalizePayload(parsed)
if canonical["third_order_id"] != "PAY1" || canonical["provider_order_id"] != "SB1" ||
canonical["amount"] != "100" || canonical["status"] != "paid" {
t.Fatalf("canonical notify = %#v", canonical)
}
if result := NewClient(Config{AccessToken: "token-1"}).VerifyNotify(parsed); !result.OK {
t.Fatalf("VerifyNotify() failed: got=%s expected=%v", result.Got, result.Expected)
}
parsed["total_fee"] = "101"
if NewClient(Config{AccessToken: "token-1"}).VerifyNotify(parsed).OK {
t.Fatal("VerifyNotify() accepted a tampered amount")
}
}
func TestRefundAndRefundQueryStatusMapping(t *testing.T) {
responses := []map[string]string{
{
"return_code": "01",
"return_msg": "退款成功",
"result_code": "01",
"merchant_no": "M1",
"refund_fee": "100",
"out_refund_no": "RF1",
},
{
"return_code": "01",
"return_msg": "退款查询成功",
"result_code": "01",
"merchant_no": "M1",
"refund_fee": "100",
"out_refund_no": "RF1",
"trade_state": "SUCCESS",
"end_time": "20261111121000",
},
}
var index int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response := responses[index]
index++
response["key_sign"] = Sign(response, "token-1")
_ = json.NewEncoder(w).Encode(response)
}))
defer server.Close()
client := NewClient(Config{
GatewayURL: server.URL,
MerchantID: "M1",
TerminalID: "T1",
AccessToken: "token-1",
})
refund, err := client.CreateRefund(context.Background(), CreateRefundRequest{
ProviderOrderID: "SB1",
MerchantRefundID: "REF1",
RefundAmountCent: 100,
})
if err != nil {
t.Fatalf("CreateRefund() error = %v", err)
}
if refund.Status != "refunded" || refund.ProviderRefundID != "RF1" {
t.Fatalf("CreateRefund() response = %+v", refund)
}
query, err := client.QueryRefund(context.Background(), QueryRefundRequest{ProviderRefundID: "RF1"})
if err != nil {
t.Fatalf("QueryRefund() error = %v", err)
}
if query.Status != "refunded" || query.RefundTime == "" {
t.Fatalf("QueryRefund() response = %+v", query)
}
}