package gateway_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "time" "github.com/wangjia/pay/config" "github.com/wangjia/pay/internal/accounts" "github.com/wangjia/pay/internal/gateway" "github.com/wangjia/pay/internal/model" "github.com/wangjia/pay/internal/provider" "github.com/wangjia/pay/internal/provider/crypto" "github.com/wangjia/pay/internal/store" ) // TestE2ECryptoQuerySettles 验证真 crypto adapter(非 fake)跑通 P2 全链: // 下单(单地址+唯一金额)→ 查单兜底(假 TronGrid only_confirmed 精确匹配)→ 入账 → webhook。 // 不经 providerbuild.BuildRegistry(那是 main 装配期用的),直接用 crypto.New 组 gateway—— // 本用例验证的是「真 adapter 跑通管线」,BuildRegistry 本身的装配/跳过决策由 // internal/providerbuild/registry_build_test.go 的表驱动用例覆盖。 func TestE2ECryptoQuerySettles(t *testing.T) { const addr = "TWe2eADDRESS00000000000000000000000" t.Setenv("E2E_ADDRESS", addr) t.Setenv("E2E_TRONGRID_KEY", "k") // 假 TronGrid:已确认、金额精确匹配、块时晚于建单的转账(期望金额在下单后从 payload 取)。 var expected int64 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := map[string]any{"success": true, "data": []map[string]any{{ "transaction_id": "tx-e2e", "to": addr, "type": "Transfer", "value": strconv.FormatInt(expected, 10), "block_timestamp": time.Now().Add(5 * time.Second).UnixMilli(), // 晚于建单(查单在下单后发生) }}} _ = json.NewEncoder(w).Encode(resp) })) defer ts.Close() db := model.OpenTestDB(t) orders := store.NewOrderStore(db) acctReg := accounts.New([]config.AccountConfig{ {AccountID: "e2e-1", Channel: "crypto", Enabled: true, Region: "global", CredentialEnvPrefix: "e2e"}, }) preg := provider.NewRegistry() preg.Register(crypto.New(acctReg, crypto.WithBaseURL(ts.URL), crypto.WithHTTPClient(ts.Client()))) // gateway.New 要求 accounts.Picker(非裸 *accounts.Registry);单账户场景用默认 // round_robin 的 Router 包一层即可(同 gateway_test.go newGateway 的装法)。 picker := accounts.NewRouter(acctReg, nil, nil) spy := &spyEnqueuer{} g := gateway.New(orders, preg, picker, cryptoResolver{}, spy, "global") // 下单 → 从 session payload 拿到期望链上金额(base+唯一尾数),喂给假 TronGrid。 res, err := g.CreateOrder(context.Background(), gateway.CreateOrderInput{ SKU: "pro_year", Method: "crypto", BizSystem: "pangolin", BizRef: "u-e2e", }) if err != nil { t.Fatalf("create: %v", err) } atts, _ := orders.ListAttemptsByStatus(model.AttemptPending, 10) if len(atts) != 1 { t.Fatalf("want 1 pending attempt, got %d", len(atts)) } expected = res.Session.Payload["amount_minor"].(int64) // 唯一金额(随机尾数)只有 payload/provider_ref 知道 // 查单兜底 → 命中 → 入账 → webhook n, err := g.SyncPendingAttempts(context.Background(), 10) if err != nil || n != 1 { t.Fatalf("sync settled=%d err=%v want 1", n, err) } o, _ := orders.GetOrder(res.OrderNo) if o.Status != model.OrderPaidV2 { t.Fatalf("order status = %s want paid", o.Status) } if len(spy.calls) != 1 || spy.calls[0]["product_biz_code"] != "pro_year" { t.Fatalf("webhook = %+v", spy.calls) } } type cryptoResolver struct{} func (cryptoResolver) Resolve(sku, currency string) (int64, string, string, error) { if sku == "pro_year" && currency == "USDT" { return 29990000, "Pro 年付", "pro_year", nil } return 0, "", "", gateway.ErrProductNotFound }