package pay import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/wangjia/pangolin/server/internal/codes" ) // newHandlerRig: 假 pay + sqlite 台账 + chi 路由(URL 参数解析需要真实路由)。 func newHandlerRig(t *testing.T, payFn http.HandlerFunc) (*chi.Mux, *Store) { t.Helper() db := openMigratedSQLite(t) seedUser(t, db, 1, "uuid-1") seedUser(t, db, 2, "uuid-2") if payFn == nil { payFn = func(http.ResponseWriter, *http.Request) {} } srv := fakePay(t, payFn) st := NewStore(db) h := NewHandler(NewClient(srv.URL, "pangolin", testSecret), st, db) r := chi.NewRouter() r.Get("/v1/pay/catalog", h.Catalog) r.Post("/v1/pay/orders", h.CreateOrder) r.Get("/v1/pay/orders/{orderNo}", h.GetOrder) r.Post("/v1/pay/orders/{orderNo}/retry", h.Retry) r.Post("/v1/pay/orders/{orderNo}/cancel", h.Cancel) return r, st } // authed 注入 uid(auth.RequireAuth 注入的就是 codes.CtxKeyUserID)。 func authed(r *http.Request, uid int64) *http.Request { return r.WithContext(context.WithValue(r.Context(), codes.CtxKeyUserID, uid)) } func TestCreateOrder_ProxiesAndRecords(t *testing.T) { router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"data":{"order_no":"pay001","session":{ "render_type":"redirect","payload":{"url":"https://alipay.example/x"}}}}`)) }) body := []byte(`{"sku":"pro_month","method":"alipay","metadata":{"is_mobile":"1","evil":"x"}}`) req := authed(httptest.NewRequest(http.MethodPost, "/v1/pay/orders", bytes.NewReader(body)), 1) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("code = %d, body = %s", w.Code, w.Body) } var resp struct { OrderNo string `json:"order_no"` Session Session `json:"session"` } _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp.OrderNo != "pay001" || resp.Session.RenderType != "redirect" { t.Fatalf("resp = %+v", resp) } row, err := st.GetForUser(context.Background(), 1, "pay001") if err != nil { t.Fatalf("台账未落: %v", err) } if row.SKU != "pro_month" || row.BizRef != "uuid-1" || row.Status != "created" { t.Fatalf("台账行不符: %+v", row) } } func TestCreateOrder_UnknownSKU400(t *testing.T) { router, _ := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) { t.Fatal("不该打到 pay") }) body := []byte(`{"sku":"pro_lifetime","method":"alipay"}`) req := authed(httptest.NewRequest(http.MethodPost, "/v1/pay/orders", bytes.NewReader(body)), 1) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("code = %d", w.Code) } } func TestGetOrder_OwnershipEnforced(t *testing.T) { router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"data":{"order_no":"pay001","status":"pending", "subject":"s","amount_minor":2999,"currency":"CNY"}}`)) }) if err := st.Insert(context.Background(), 1, "uuid-1", "pro_month", "pay001", "alipay"); err != nil { t.Fatal(err) } // 属主可查 w := httptest.NewRecorder() router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodGet, "/v1/pay/orders/pay001", nil), 1)) if w.Code != http.StatusOK { t.Fatalf("owner code = %d", w.Code) } var resp struct { PayStatus string `json:"pay_status"` Activated bool `json:"activated"` } _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp.PayStatus != "pending" || resp.Activated { t.Fatalf("resp = %+v", resp) } // 他人 404 w2 := httptest.NewRecorder() router.ServeHTTP(w2, authed(httptest.NewRequest(http.MethodGet, "/v1/pay/orders/pay001", nil), 2)) if w2.Code != http.StatusNotFound { t.Fatalf("other code = %d", w2.Code) } } func TestRetry_CurrencyMismatchMapped409(t *testing.T) { router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusConflict) _, _ = w.Write([]byte(`{"code":"currency_mismatch","message":"换渠道需新单"}`)) }) _ = st.Insert(context.Background(), 1, "uuid-1", "pro_month", "pay001", "crypto") body := []byte(`{"method":"alipay"}`) w := httptest.NewRecorder() router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodPost, "/v1/pay/orders/pay001/retry", bytes.NewReader(body)), 1)) if w.Code != http.StatusConflict { t.Fatalf("code = %d", w.Code) } var e struct { Code string `json:"code"` } _ = json.Unmarshal(w.Body.Bytes(), &e) if e.Code != "CURRENCY_MISMATCH" { t.Fatalf("code = %q, want CURRENCY_MISMATCH", e.Code) } } func TestCatalog(t *testing.T) { router, _ := newHandlerRig(t, nil) w := httptest.NewRecorder() router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodGet, "/v1/pay/catalog", nil), 1)) var resp struct { Items []CatalogItem `json:"items"` } _ = json.Unmarshal(w.Body.Bytes(), &resp) if len(resp.Items) != 3 || resp.Items[0].SKU != "pro_month" { t.Fatalf("catalog = %+v", resp.Items) } }