package handler_test import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/wangjia/pay/config" "github.com/wangjia/pay/internal/accounts" "github.com/wangjia/pay/internal/gateway" "github.com/wangjia/pay/internal/handler" "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/webhook" ) // buildDevMockEngine 装配一个带 DevMockHandler 的最小 gin engine:与 main.go 的 // MOCK_CHANNEL_ENABLED 挂载点(POST /api/v2/dev/orders/:order_no/mark-paid)同形, // 但走真实 webhook.Notifier + store.WebhookStore(而非 nopEnqueuer),这样才能断言 // mark-paid 触发的 webhook 出站 outbox 真的入队了一行——handler 包既有的 // buildEngine()/buildEngineWithStore() 都用 nopEnqueuer,查不到 outbox。 func buildDevMockEngine(t *testing.T) (*gin.Engine, *gateway.Gateway, *store.OrderStore, *store.WebhookStore) { t.Helper() gin.SetMode(gin.TestMode) db := model.OpenTestDB(t) orders := store.NewOrderStore(db) refunds := store.NewRefundStore(db) subs := store.NewSubscriptionStore(db) chargebacks := store.NewChargebackStore(db) webhookStore := store.NewWebhookStore(db) preg := provider.NewRegistry() preg.Register(fake.New()) areg := accounts.New([]config.AccountConfig{ {AccountID: "fake-a1", Channel: "fake", Region: "global", Enabled: true, Weight: 1}, }) picker := accounts.NewRouter(areg, nil, nil) // bizConfig/orderPaid 只是 Notifier 投递(DeliverPending)才用得到;本测试只验证 // Enqueue 落 outbox 一行,不跑投递循环,给最简单的桩即可。 notifier := webhook.NewNotifier(webhookStore, func(string) (config.BizSystemConfig, bool) { return config.BizSystemConfig{}, true }, func(string) (bool, error) { return true, nil }, ) g := gateway.New(orders, refunds, preg, picker, oneResolver{}, notifier, "global", subs, chargebacks) devH := handler.NewDevMockHandler(db, g) r := gin.New() r.POST("/api/v2/dev/orders/:order_no/mark-paid", devH.MarkPaid) return r, g, orders, webhookStore } // TestDevMockMarkPaid 覆盖 dev-only「模拟付款成功」端点的最小闭环:fake 渠道下单 // (带 biz_system,否则 Settle 不会入队 webhook,见 gateway/settle.go // enqueuePaymentSucceeded)→ POST mark-paid → 200 + 订单翻 paid + webhook outbox // 有一行 payment.succeeded 待投递。 func TestDevMockMarkPaid(t *testing.T) { r, g, orders, webhookStore := buildDevMockEngine(t) ctx := t.Context() res, err := g.CreateOrder(ctx, gateway.CreateOrderInput{ SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-1", }) if err != nil { t.Fatalf("CreateOrder: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/v2/dev/orders/"+res.OrderNo+"/mark-paid", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("mark-paid code=%d body=%s", w.Code, w.Body.String()) } o, err := orders.GetOrder(res.OrderNo) if err != nil { t.Fatalf("GetOrder: %v", err) } if o.Status != model.OrderPaidV2 { t.Fatalf("订单状态 = %v, want paid", o.Status) } rows, err := webhookStore.ListUndelivered(10) if err != nil { t.Fatalf("ListUndelivered: %v", err) } found := false for _, row := range rows { if row.OutTradeNo == res.OrderNo && row.EventType == "payment.succeeded" { found = true } } if !found { t.Fatalf("webhook outbox 应有一行 %s/payment.succeeded 待投递, rows=%+v", res.OrderNo, rows) } } // TestDevMockMarkPaidOrderNotFound 覆盖找不到订单(未走 fake 渠道下单/order_no 错) // 时的 404 分支,不误判成 500。 func TestDevMockMarkPaidOrderNotFound(t *testing.T) { r, _, _, _ := buildDevMockEngine(t) req := httptest.NewRequest(http.MethodPost, "/api/v2/dev/orders/GHOST-ORDER/mark-paid", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("未知订单 mark-paid code=%d, want 404, body=%s", w.Code, w.Body.String()) } }