Build and Publish Server / build (push) Successful in 4m30s
- 新增商业化模块: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
153 lines
4.4 KiB
Go
153 lines
4.4 KiB
Go
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")
|
|
}
|
|
}
|