da8bbefe2e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u # Conflicts: # internal/model/testdb.go # internal/provider/provider.go # internal/router/router.go # internal/store/order_query_test.go # main.go
102 lines
3.7 KiB
Go
102 lines
3.7 KiB
Go
package handler_test
|
|
|
|
import (
|
|
"net/http"
|
|
"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/model"
|
|
"github.com/wangjia/pay/internal/provider"
|
|
"github.com/wangjia/pay/internal/provider/fake"
|
|
"github.com/wangjia/pay/internal/router"
|
|
"github.com/wangjia/pay/internal/store"
|
|
)
|
|
|
|
// jiu 反馈波 item 3:v2 改状态端点(POST /orders 等)超过 config.RateLimit 设定的
|
|
// per-IP 速率应回 429。config.C 用完整重赋值 + defer 恢复(与 refund_test.go
|
|
// buildRefundEngine 同一套约定),避免污染同包其它测试的全局配置状态。
|
|
func TestV2CreateOrderRateLimited(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
prev := config.C
|
|
config.C = config.Config{RateLimit: config.RateLimitConfig{RequestsPerMin: 2}}
|
|
defer func() { config.C = prev }()
|
|
|
|
db := model.OpenTestDB(t)
|
|
orders := store.NewOrderStore(db)
|
|
refunds := store.NewRefundStore(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)
|
|
g := gateway.New(orders, refunds, preg, picker, oneResolver{}, nopEnqueuer{}, "global", store.NewSubscriptionStore(db), store.NewChargebackStore(db))
|
|
r := gin.New()
|
|
router.SetupV2(r, g)
|
|
|
|
body := map[string]any{"sku": "pro_year", "method": "fake"}
|
|
var codes []int
|
|
for i := 0; i < 3; i++ {
|
|
w, _ := do(t, r, http.MethodPost, "/api/v2/orders", body)
|
|
codes = append(codes, w.Code)
|
|
}
|
|
if codes[0] != http.StatusOK || codes[1] != http.StatusOK {
|
|
t.Fatalf("前 2 次(= burst=RequestsPerMin)应放行, got codes=%v", codes)
|
|
}
|
|
if codes[2] != http.StatusTooManyRequests {
|
|
t.Fatalf("第 3 次(超 burst)应 429, got codes=%v", codes)
|
|
}
|
|
}
|
|
|
|
// callback 端点(渠道来源,不是终端用户)不挂限流:哪怕下单类速率上限压得很低,
|
|
// 回调也不该被误伤,否则会把渠道异步通知的正常重投当成攻击拦掉。
|
|
func TestV2CallbackNotRateLimited(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
prev := config.C
|
|
config.C = config.Config{RateLimit: config.RateLimitConfig{RequestsPerMin: 1}}
|
|
defer func() { config.C = prev }()
|
|
|
|
r, _ := buildEngineWithStore(t)
|
|
for i := 0; i < 5; i++ {
|
|
w, _ := do(t, r, http.MethodPost, "/api/v2/callback/fake", map[string]any{
|
|
"provider_ref": "GHOST", "status": "succeeded", "amount_minor": 1, "currency": "USDT",
|
|
})
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("callback 第 %d 次不应被限流, code=%d", i+1, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// disabled:true 应完全放行改状态端点,不管速率配多低。
|
|
func TestV2RateLimitDisabled(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
prev := config.C
|
|
config.C = config.Config{RateLimit: config.RateLimitConfig{Disabled: true, RequestsPerMin: 1}}
|
|
defer func() { config.C = prev }()
|
|
|
|
db := model.OpenTestDB(t)
|
|
orders := store.NewOrderStore(db)
|
|
refunds := store.NewRefundStore(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)
|
|
g := gateway.New(orders, refunds, preg, picker, oneResolver{}, nopEnqueuer{}, "global", store.NewSubscriptionStore(db), store.NewChargebackStore(db))
|
|
r := gin.New()
|
|
router.SetupV2(r, g)
|
|
|
|
body := map[string]any{"sku": "pro_year", "method": "fake"}
|
|
for i := 0; i < 5; i++ {
|
|
w, _ := do(t, r, http.MethodPost, "/api/v2/orders", body)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("disabled=true 时第 %d 次不应被限流, code=%d", i+1, w.Code)
|
|
}
|
|
}
|
|
}
|