Files
pay/internal/provider/crypto/crypto_test.go
T

144 lines
4.7 KiB
Go

package crypto_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
"github.com/wangjia/pay/config"
"github.com/wangjia/pay/internal/accounts"
"github.com/wangjia/pay/internal/provider"
"github.com/wangjia/pay/internal/provider/crypto"
)
const addr = "TWreceiveADDRESS0000000000000000000"
func newProv(t *testing.T, ts *httptest.Server) *crypto.Provider {
t.Setenv("CRY_ADDRESS", addr)
t.Setenv("CRY_TRONGRID_KEY", "test-key")
reg := accounts.New([]config.AccountConfig{
{AccountID: "cry-1", Channel: "crypto", Enabled: true, CredentialEnvPrefix: "cry"},
})
return crypto.New(reg, crypto.WithBaseURL(ts.URL), crypto.WithHTTPClient(ts.Client()))
}
// 唯一金额分配(canonical allocateAmount 语义):同价并发单分到不同尾数金额,
// 尾数 ∈ [1,9999] 微USDT,且经 ProviderRef 往返。
func TestCreateAllocatesUniqueTailedAmount(t *testing.T) {
ts := httptest.NewServer(http.NotFoundHandler()) // Create 不打网
defer ts.Close()
p := newProv(t, ts)
mk := func(outNo string) *provider.Session {
sess, err := p.Create(context.Background(), provider.CreateRequest{
OutTradeNo: outNo, AmountMinor: 29990000, Currency: "USDT",
Account: config.AccountConfig{AccountID: "cry-1", CredentialEnvPrefix: "cry"},
})
if err != nil {
t.Fatalf("create %s: %v", outNo, err)
}
return sess
}
s1, s2 := mk("PAY-A"), mk("PAY-B")
if s1.RenderType != provider.RenderCryptoAddress {
t.Fatalf("render_type = %s", s1.RenderType)
}
if s1.Payload["address"] != addr {
t.Fatalf("address = %v want %s", s1.Payload["address"], addr)
}
a1, _ := s1.Payload["amount_minor"].(int64)
a2, _ := s2.Payload["amount_minor"].(int64)
for _, a := range []int64{a1, a2} {
if a <= 29990000 || a > 29990000+9999 {
t.Fatalf("amount_minor = %d 不在 (base, base+9999]", a)
}
}
if a1 == a2 {
t.Fatalf("冷却窗内两单分到同一唯一金额: %d(预留失效)", a1)
}
// 尾数编进 provider_ref,Query 端可复原期望金额
if want := fmt.Sprintf("CRYPTO-PAY-A-%d", a1-29990000); s1.ProviderRef != want {
t.Fatalf("provider_ref = %s want %s", s1.ProviderRef, want)
}
if s1.ExpiresAt == nil {
t.Fatal("应带 15min 支付窗 ExpiresAt")
}
}
// canonical watcher 匹配:only_confirmed + 精确金额 + 块时晚于建单。
func TestQueryMatchesConfirmedExactAmount(t *testing.T) {
const base, tail = int64(29990000), int64(777)
expected := base + tail
created := time.Now().Add(-10 * time.Minute)
req := provider.QueryRequest{
ProviderRef: "CRYPTO-PAY-A-777", OutTradeNo: "PAY-A", AccountID: "cry-1",
AmountMinor: base, Currency: "USDT", CreatedAt: created,
}
run := func(value, blockMs int64) (*provider.PaidEvent, url.Values) {
var seen url.Values
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen = r.URL.Query()
if r.Header.Get("TRON-PRO-API-KEY") == "" {
http.Error(w, "no key", http.StatusUnauthorized)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
"data": []map[string]any{{
"transaction_id": "tx1",
"to": addr,
"type": "Transfer",
"value": strconv.FormatInt(value, 10),
"block_timestamp": blockMs, // 毫秒(TronGrid 口径),adapter 内 /1000 成秒
}},
})
}))
defer ts.Close()
p := newProv(t, ts)
ev, err := p.Query(context.Background(), req)
if err != nil {
t.Fatalf("query: %v", err)
}
return ev, seen
}
// A. 已确认 + 金额精确 + 块时晚于建单 → succeeded
ev, seen := run(expected, time.Now().Add(-2*time.Minute).UnixMilli())
if ev.Status != provider.PaidSucceeded || ev.PaidAmountMinor != expected || ev.PaidCurrency != "USDT" {
t.Fatalf("A: event = %+v", ev)
}
if ev.PaidAt == nil || ev.Raw != "tx1" {
t.Fatalf("A: PaidAt/Raw = %+v", ev)
}
if seen.Get("only_confirmed") != "true" {
t.Fatalf("必须带 only_confirmed=true(canonical 确认标准), query = %v", seen)
}
// B. 金额差 1 微USDT → pending(唯一金额精确匹配,不误判)
if ev, _ := run(expected-1, time.Now().Add(-2*time.Minute).UnixMilli()); ev.Status != provider.PaidPending {
t.Fatalf("B: 金额不符应 pending, got %+v", ev)
}
// C. 块时早于建单(迟到旧款/金额复用场景)→ pending
if ev, _ := run(expected, created.Add(-time.Hour).UnixMilli()); ev.Status != provider.PaidPending {
t.Fatalf("C: 块时早于建单应 pending, got %+v", ev)
}
}
func TestVerifyCallbackNotSupported(t *testing.T) {
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
p := newProv(t, ts)
if _, err := p.VerifyCallback(context.Background(), provider.CallbackInput{Raw: []byte("{}")}); err == nil {
t.Fatal("crypto 无异步回调,VerifyCallback 应返回 ErrNotSupported")
}
}