- 新增商业化模块:Stripe/Binance Pay 支付、邮箱登录、订单管理 - 新增数据模型:ReportProduct, ReportEntitlement, ReportSnapshot, PaymentEvent, EmailLoginCode - 新增 API:/api/commerce/checkout, /api/commerce/webhooks/*, /api/commerce/orders/* - 新增管理后台:订单列表、统计图表、统一 AdminHeader 组件 - 新增中间件测试:RateLimit, Auth, CORS 共 7 个测试用例 - 新增 AGENTS.md 项目代理指令文件 - 新增 REMINDER.md 商业化功能开发规范 - 修复 JWT 解析:添加 WithValidMethods 限制 HS256 - 修复 GetPayStatus:增加用户权限校验 - 更新 .gitignore 和 README.md
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestRateLimitAllowsUnderLimit(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(RateLimit())
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.RemoteAddr = "192.168.1.1:12345"
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("request %d should be allowed, got %d", i+1, resp.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimitBlocksOverLimit(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(RateLimit())
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
// 发送 11 个请求,第 11 个应被限流
|
||||
for i := 0; i < 11; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.RemoteAddr = "192.168.1.2:12345"
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
if i < 10 && resp.Code != http.StatusOK {
|
||||
t.Fatalf("request %d should be allowed, got %d", i+1, resp.Code)
|
||||
}
|
||||
if i == 10 && resp.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("request 11 should be rate limited, got %d", resp.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimitWindowReset(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(RateLimit())
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
// 先发送 10 个请求达到上限
|
||||
for i := 0; i < 10; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.RemoteAddr = "192.168.1.3:12345"
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
}
|
||||
|
||||
// 模拟时间过去 61 秒(超过 1 分钟窗口)
|
||||
// 注意:由于实现使用真实时间,这里只能验证当前窗口内的行为
|
||||
// 实际窗口重置测试需要 mock 时间或等待
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.RemoteAddr = "192.168.1.3:12345"
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("should still be rate limited within window, got %d", resp.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthRejectsMissingToken(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(Auth())
|
||||
router.GET("/protected", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", resp.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthRejectsMalformedToken(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(Auth())
|
||||
router.GET("/protected", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
||||
req.Header.Set("Authorization", "InvalidFormat token123")
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", resp.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSAllowsConfiguredOrigin(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
t.Setenv("CORS_ORIGINS", "https://example.com,https://app.example.com")
|
||||
router := gin.New()
|
||||
router.Use(CORS())
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.Code)
|
||||
}
|
||||
if resp.Header().Get("Access-Control-Allow-Origin") != "https://example.com" {
|
||||
t.Fatalf("expected CORS header for allowed origin, got %s", resp.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSRejectsUnconfiguredOrigin(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
t.Setenv("CORS_ORIGINS", "https://example.com")
|
||||
router := gin.New()
|
||||
router.Use(CORS())
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
|
||||
req.Header.Set("Origin", "https://evil.com")
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
// 非白名单源不应返回 CORS 头
|
||||
if resp.Header().Get("Access-Control-Allow-Origin") == "https://evil.com" {
|
||||
t.Fatal("should not allow unconfigured origin")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user