130 lines
4.2 KiB
Go
130 lines
4.2 KiB
Go
package pay
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const testSecret = "test-biz-secret"
|
|
|
|
// fakePay 模拟 pay v2:create 按 pay verifyBizSign 语义验签(±300s+HMAC),
|
|
// 其余端点免签(与 pay 一致:仅 create 且 biz_system 非空时验签)。
|
|
func fakePay(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
|
t.Helper()
|
|
srv := httptest.NewServer(handler)
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func verifyCreateSign(t *testing.T, r *http.Request, body []byte) {
|
|
t.Helper()
|
|
system := r.Header.Get("X-Pay-System")
|
|
ts := r.Header.Get("X-Pay-Timestamp")
|
|
nonce := r.Header.Get("X-Pay-Nonce")
|
|
sign := r.Header.Get("X-Pay-Sign")
|
|
if system != "pangolin" || ts == "" || nonce == "" || sign == "" {
|
|
t.Fatalf("签名头缺失: system=%q ts=%q nonce=%q sign=%q", system, ts, nonce, sign)
|
|
}
|
|
tsi, err := strconv.ParseInt(ts, 10, 64)
|
|
if err != nil {
|
|
t.Fatalf("ts 非 unix 秒: %v", err)
|
|
}
|
|
if d := time.Now().Unix() - tsi; d > 300 || d < -300 {
|
|
t.Fatalf("ts 超 ±300s 窗口: %d", d)
|
|
}
|
|
if !hmacVerify(testSecret, sign, system, ts, nonce, string(body)) {
|
|
t.Fatal("HMAC 校验失败(须为 std base64 + \\n join)")
|
|
}
|
|
}
|
|
|
|
func TestCreateOrder_SignsAndParses(t *testing.T) {
|
|
srv := fakePay(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost || r.URL.Path != "/api/v2/orders" {
|
|
t.Fatalf("意外请求: %s %s", r.Method, r.URL.Path)
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
verifyCreateSign(t, r, body)
|
|
var req map[string]any
|
|
_ = json.Unmarshal(body, &req)
|
|
if req["sku"] != "pro_month" || req["method"] != "crypto" ||
|
|
req["biz_system"] != "pangolin" || req["biz_ref"] != "uuid-1" {
|
|
t.Fatalf("下单 body 不符: %v", req)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"data":{"order_no":"pay123","session":{
|
|
"render_type":"crypto_address",
|
|
"payload":{"address":"Txx","amount":"4.201234","amount_minor":4201234,
|
|
"currency":"USDT","network":"TRC20","contract":"Tcc"},
|
|
"expires_at":"2026-07-10T12:00:00Z"}}}`))
|
|
})
|
|
c := NewClient(srv.URL, "pangolin", testSecret)
|
|
res, err := c.CreateOrder(t.Context(), "pro_month", "crypto", "uuid-1", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateOrder: %v", err)
|
|
}
|
|
if res.OrderNo != "pay123" || res.Session.RenderType != "crypto_address" {
|
|
t.Fatalf("解析不符: %+v", res)
|
|
}
|
|
if got := res.Session.Payload["address"]; got != "Txx" {
|
|
t.Errorf("payload.address = %v", got)
|
|
}
|
|
if res.Session.ExpiresAt == nil {
|
|
t.Error("expires_at 未解析")
|
|
}
|
|
}
|
|
|
|
func TestGetOrder_ParsesStatus(t *testing.T) {
|
|
srv := fakePay(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v2/orders/pay123" {
|
|
t.Fatalf("path = %s", r.URL.Path)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"order_no":"pay123","status":"succeeded",
|
|
"subject":"Pangolin 专业版·月付","amount_minor":4201234,"currency":"USDT",
|
|
"paid_at":"2026-07-10T11:00:00Z"}}`))
|
|
})
|
|
c := NewClient(srv.URL, "pangolin", testSecret)
|
|
st, err := c.GetOrder(t.Context(), "pay123")
|
|
if err != nil {
|
|
t.Fatalf("GetOrder: %v", err)
|
|
}
|
|
if st.Status != "succeeded" || st.AmountMinor != 4201234 || st.PaidAt == nil {
|
|
t.Fatalf("解析不符: %+v", st)
|
|
}
|
|
}
|
|
|
|
func TestRetry_CurrencyMismatchMapsToError(t *testing.T) {
|
|
srv := fakePay(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v2/orders/pay123/retry" {
|
|
t.Fatalf("path = %s", r.URL.Path)
|
|
}
|
|
w.WriteHeader(http.StatusConflict)
|
|
_, _ = w.Write([]byte(`{"code":"currency_mismatch","message":"换渠道需新单"}`))
|
|
})
|
|
c := NewClient(srv.URL, "pangolin", testSecret)
|
|
_, err := c.Retry(t.Context(), "pay123", "alipay", nil)
|
|
var pe *Error
|
|
if !errors.As(err, &pe) || pe.Code != "currency_mismatch" || pe.HTTPStatus != http.StatusConflict {
|
|
t.Fatalf("err = %v, want *pay.Error{409 currency_mismatch}", err)
|
|
}
|
|
}
|
|
|
|
func TestCancel(t *testing.T) {
|
|
srv := fakePay(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v2/orders/pay123/cancel" {
|
|
t.Fatalf("path = %s", r.URL.Path)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"canceled":true}}`))
|
|
})
|
|
c := NewClient(srv.URL, "pangolin", testSecret)
|
|
ok, err := c.Cancel(t.Context(), "pay123")
|
|
if err != nil || !ok {
|
|
t.Fatalf("Cancel = %v, %v", ok, err)
|
|
}
|
|
}
|