package gateway_test import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "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/fake" "github.com/wangjia/pay/internal/store" "github.com/wangjia/pay/internal/util" "github.com/wangjia/pay/internal/webhook" ) // TestE2EFullChainCallbackToDeliveredWebhook 补齐「下单 → 回调 → settle → webhook // 实际 HTTP 投递 → 标 delivered」整链(各层此前只独立测过): // - 起一个 httptest.Server 当业务方回调接收器,记录收到的请求,验证 X-Pay-* 头齐全 + // 能用同一 secret 验签通过(出站签名互操作,与 internal/webhook/notifier_test.go 同法)。 // - fake 渠道下单 → 模拟渠道成功回调(HandleCallback,同 settle_test.go // TestHandleCallbackAndSync 的回调体) → Settle 翻转 paid → outbox 入队。 // - 用真实 store.WebhookStore + webhook.Notifier(非 spyEnqueuer)驱动一次 // DeliverPending(即 notifier 的投递 RunOnce)→ 断言 webhook 真被投到 test server、 // server 收到的 payload 签名正确、outbox 行标记 delivered。 func TestE2EFullChainCallbackToDeliveredWebhook(t *testing.T) { const secret = "e2e-fullchain-secret" const bizSystem = "pangolin" var gotBody []byte var gotHeaders http.Header var hits int biz := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits++ gotBody, _ = io.ReadAll(r.Body) gotHeaders = r.Header.Clone() w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("SUCCESS")) })) defer biz.Close() db := model.OpenTestDB(t) orders := store.NewOrderStore(db) refunds := store.NewRefundStore(db) subs := store.NewSubscriptionStore(db) chargebacks := store.NewChargebackStore(db) preg := provider.NewRegistry() fp := fake.New() preg.Register(fp) areg := accounts.New([]config.AccountConfig{ {AccountID: "fake-a1", Channel: "fake", Region: "global", Enabled: true, Weight: 1}, }) picker := accounts.NewRouter(areg, nil, nil) ws := store.NewWebhookStore(db) // bizConfig:biz_system "pangolin" 的回调地址指向这台 test server(生产由 // config.C.BizByName 按业务方名下发,这里直接指定同等语义)。 bizCfg := func(system string) (config.BizSystemConfig, bool) { if system == bizSystem { return config.BizSystemConfig{CallbackURL: biz.URL, Secret: secret}, true } return config.BizSystemConfig{}, false } // orderPaid 门禁走真实订单查询(不像多数单测那样恒真),验证投递门禁与真实订单状态联动。 orderPaid := func(outTradeNo string) (bool, error) { o, err := orders.GetOrder(outTradeNo) if err != nil { return false, err } return o.Status == model.OrderPaidV2, nil } notifier := webhook.NewNotifier(ws, bizCfg, orderPaid) g := gateway.New(orders, refunds, preg, picker, stubResolver{}, notifier, "global", subs, chargebacks) ctx := context.Background() res, err := g.CreateOrder(ctx, gateway.CreateOrderInput{ SKU: "pro_year", Method: "fake", BizSystem: bizSystem, BizRef: "u-e2e-full", }) if err != nil { t.Fatalf("create order: %v", err) } ref := attemptRef(t, orders) // 渠道方成功回调(同 TestHandleCallbackAndSync 的回调体)→ VerifyCallback → Settle。 body, _ := json.Marshal(map[string]any{ "provider_ref": ref, "status": "succeeded", "amount_minor": 29990000, "currency": "USDT", }) got, err := g.HandleCallback(ctx, "fake", provider.CallbackInput{Raw: body}) if err != nil || got != gateway.SettleProcessed { t.Fatalf("HandleCallback = %v, %v, want SettleProcessed", got, err) } o, err := orders.GetOrder(res.OrderNo) if err != nil || o.Status != model.OrderPaidV2 { t.Fatalf("order = %+v, %v, want paid", o, err) } // 入队后未投递:确认 outbox 里躺着一条待发行。 pend, err := ws.ListUndelivered(10) if err != nil || len(pend) != 1 || pend[0].OutTradeNo != res.OrderNo { t.Fatalf("undelivered = %+v, %v, want 1 row for %s", pend, err, res.OrderNo) } // notifier 投递一轮(RunOnce 语义)→ 真实 HTTP POST 到业务方接收器。 sent, err := notifier.DeliverPending(10) if err != nil || sent != 1 { t.Fatalf("DeliverPending = %d, %v, want 1", sent, err) } if hits != 1 { t.Fatalf("业务方应恰收到 1 次 POST, got %d", hits) } // 出站签名头齐全 + 能用同一 secret 验签通过(互操作校验,业务方即用同法验签)。 sys := gotHeaders.Get("X-Pay-System") ev := gotHeaders.Get("X-Pay-Event") ts := gotHeaders.Get("X-Pay-Timestamp") nonce := gotHeaders.Get("X-Pay-Nonce") sign := gotHeaders.Get("X-Pay-Sign") if sys != bizSystem || ev != "payment.succeeded" || ts == "" || nonce == "" || sign == "" { t.Fatalf("X-Pay-* 头不全: system=%q event=%q ts=%q nonce=%q sign=%q", sys, ev, ts, nonce, sign) } if !util.HMACVerify(secret, sign, sys, ts, nonce, string(gotBody)) { t.Fatalf("业务方侧验签失败(出站签名与 HMACVerify 不互操作)") } var payload map[string]any if err := json.Unmarshal(gotBody, &payload); err != nil { t.Fatalf("payload 非法 JSON: %v, body=%s", err, gotBody) } if payload["event_type"] != "payment.succeeded" || payload["out_trade_no"] != res.OrderNo || payload["product_biz_code"] != "pro_year" { t.Fatalf("payload = %+v", payload) } // outbox 行已标 delivered:再投一次不应重发。 pendAfter, err := ws.ListUndelivered(10) if err != nil || len(pendAfter) != 0 { t.Fatalf("投递后 undelivered = %+v, %v, want 0(已标 delivered)", pendAfter, err) } if sent2, _ := notifier.DeliverPending(10); sent2 != 0 { t.Fatalf("已投递不应重发, got %d", sent2) } if hits != 1 { t.Fatalf("重投检查不应产生新 POST, hits=%d", hits) } }