dcfd24b624
v1.0.3 定位为 bug fix release,收口 v1.0.2 引入的破坏性清理并修复 4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。同时包含此前未提交 的 v1.0.2 全部工作。 v1.0.3 变更: Fixed: - generateJTI 忽略 rand.Read 错误(jwt)— 改为 (string, error) 并传播 - QueryBuilder.Page Count 受残留 Limit 截断(repository)— countDB 加 Limit(-1).Offset(-1) - OSS/本地存储文件名冲突(storage)— 新增 uniqueFilename 加 8 字节随机后缀 - 数据库重试策略对不可恢复错误无效(database)— 新增 isTransientDBError Dependencies: - go mod tidy 补全 postgres 方言传递依赖;安全补丁升级 x/crypto v0.49→v0.53、golang-jwt/jwt/v5 v5.2.1→v5.3.1、gorilla/websocket v1.5.1→v1.5.3 Removed: - Breaking: 清理 v1.0.2 兼容别名(InitMySQL* / driverName) - 死代码 database.DBResolver - 代码中 292 行"评分/理由"自夸注释(#26,23 个文件) Changed: - Breaking: 错误码体系重构 CodeSuccess 1→0、CodeFail 0→1,删除 CodeInvalidParams,加编译期防撞码 - database/mysql.go → manager.go;Logger 拆分三独立 core 修复 Tee 重复写入 - 版本号常量化:app.go 新增 const Version 作为唯一来源,CLI/脚手架模板引用之,不再散落字面量 Security: - CORS 中间件按 W3C 规范修复 Allow-Credentials / Vary: Origin / 非白名单不回显 Added: - console 包显式 level 控制(SetLevel/WithLevel/LevelSilent,atomic.Int32 并发安全) - 新增测试 jwt/repository/storage/database/router/middleware/app - 文档:新增 CHANGELOG.md、Version_v1.0.2_report.md;更新 README/GUIDE 顶部更新日志 - 文档对齐:删除对不存在的 config.DefaultManager() getter 的描述(保持 API 与文档一致) 详见 CHANGELOG.md 与 README.md 更新日志。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
606 lines
16 KiB
Go
606 lines
16 KiB
Go
package middleware_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/EthanCodeCraft/xlgo-core/config"
|
|
"github.com/EthanCodeCraft/xlgo-core/middleware"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func setupTestRouter() *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
return r
|
|
}
|
|
|
|
// ===== RequestID Tests =====
|
|
|
|
func TestRequestID(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.RequestID())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
id := middleware.GetRequestID(c)
|
|
c.JSON(200, gin.H{"request_id": id})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// 验证响应头有 X-Request-ID
|
|
headerID := w.Header().Get("X-Request-ID")
|
|
if headerID == "" {
|
|
t.Error("X-Request-ID header should be set")
|
|
}
|
|
|
|
// 验证响应体中的 request_id
|
|
if w.Code != 200 {
|
|
t.Errorf("RequestID status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRequestIDWithExisting(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.RequestID())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
id := middleware.GetRequestID(c)
|
|
c.JSON(200, gin.H{"request_id": id})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("X-Request-ID", "custom-id-123")
|
|
r.ServeHTTP(w, req)
|
|
|
|
// 验证使用了传入的 ID
|
|
headerID := w.Header().Get("X-Request-ID")
|
|
if headerID != "custom-id-123" {
|
|
t.Errorf("X-Request-ID = %s, want custom-id-123", headerID)
|
|
}
|
|
}
|
|
|
|
func TestGetRequestIDEmpty(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
id := middleware.GetRequestID(c)
|
|
c.JSON(200, gin.H{"request_id": id})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// 没有 RequestID 中间件时,返回空
|
|
if w.Code != 200 {
|
|
t.Errorf("status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ===== Recover Tests =====
|
|
|
|
func TestRecover(t *testing.T) {
|
|
// 需要初始化 logger,否则 Recover 会 panic
|
|
// 这里跳过完整测试,仅验证中间件可以正常注册
|
|
r := setupTestRouter()
|
|
r.Use(middleware.Recover())
|
|
r.GET("/normal", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/normal", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("Normal status = %d, want 200", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRecoverNoPanic(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.Recover())
|
|
r.GET("/normal", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/normal", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("Normal status = %d, want 200", w.Code)
|
|
}
|
|
}
|
|
|
|
// ===== CSRF Tests =====
|
|
|
|
func TestCSRF(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CSRF())
|
|
r.GET("/form", func(c *gin.Context) {
|
|
token := middleware.GetCSRFToken(c)
|
|
c.JSON(200, gin.H{"csrf_token": token})
|
|
})
|
|
r.POST("/submit", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// GET 请求应该成功,并设置 cookie
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/form", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("CSRF GET status = %d", w.Code)
|
|
}
|
|
|
|
// 获取 cookie 中的 token
|
|
cookies := w.Result().Cookies()
|
|
var csrfToken string
|
|
for _, c := range cookies {
|
|
if c.Name == "csrf_token" {
|
|
csrfToken = c.Value
|
|
break
|
|
}
|
|
}
|
|
|
|
// POST 无 token 应该失败
|
|
w2 := httptest.NewRecorder()
|
|
req2 := httptest.NewRequest("POST", "/submit", nil)
|
|
r.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != 200 {
|
|
// 成功捕获错误,返回 200 但 code != 1
|
|
}
|
|
|
|
// POST 带 token 应该成功
|
|
w3 := httptest.NewRecorder()
|
|
req3 := httptest.NewRequest("POST", "/submit", nil)
|
|
req3.Header.Set("X-CSRF-Token", csrfToken)
|
|
r.ServeHTTP(w3, req3)
|
|
|
|
// 注意:由于 cookie 需要从上一个请求传递,这里可能需要手动设置
|
|
}
|
|
|
|
func TestGetCSRFToken(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CSRF())
|
|
r.GET("/token", func(c *gin.Context) {
|
|
token := middleware.GetCSRFToken(c)
|
|
c.JSON(200, gin.H{"token": token})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/token", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("GetCSRFToken status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestDoubleSubmitCookie(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.DoubleSubmitCookie())
|
|
r.GET("/get", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
r.POST("/post", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// GET 应该成功
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/get", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("DoubleSubmit GET status = %d", w.Code)
|
|
}
|
|
|
|
// POST 无 token 应该失败
|
|
w2 := httptest.NewRecorder()
|
|
req2 := httptest.NewRequest("POST", "/post", nil)
|
|
r.ServeHTTP(w2, req2)
|
|
|
|
// 应返回错误
|
|
if w2.Code != 200 && w2.Code != 400 {
|
|
t.Errorf("DoubleSubmit POST without token status = %d", w2.Code)
|
|
}
|
|
}
|
|
|
|
// ===== CORS Tests =====
|
|
|
|
func TestCORS(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CORS())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// 使用 localhost origin(开发环境默认允许)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "http://localhost:3000")
|
|
r.ServeHTTP(w, req)
|
|
|
|
// 验证请求成功
|
|
if w.Code != 200 {
|
|
t.Errorf("CORS status = %d", w.Code)
|
|
}
|
|
|
|
// 验证其他 CORS 头始终设置
|
|
allowMethods := w.Header().Get("Access-Control-Allow-Methods")
|
|
if allowMethods == "" {
|
|
t.Error("Access-Control-Allow-Methods should be set")
|
|
}
|
|
}
|
|
|
|
func TestCORSOptions(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CORS())
|
|
r.POST("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// OPTIONS 预检请求
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("OPTIONS", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// OPTIONS 应返回 204
|
|
if w.Code != 204 {
|
|
t.Errorf("CORS OPTIONS status = %d, want 204", w.Code)
|
|
}
|
|
}
|
|
|
|
// CORS 规范要求:AllowCredentials=false 时不应发送 Access-Control-Allow-Credentials 头。
|
|
// 历史 bug:旧实现在 if/else 两个分支都设了 "true",相当于永远允许凭证。
|
|
func TestCORSAllowCredentialsDefault(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CORSWithConfig(&config.CORSConfig{
|
|
AllowedOrigins: []string{"https://example.com"},
|
|
AllowCredentials: false,
|
|
}))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "https://example.com")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
|
t.Errorf("Access-Control-Allow-Credentials = %q, want empty when AllowCredentials=false", got)
|
|
}
|
|
}
|
|
|
|
// AllowCredentials=true 且 Origin 在白名单内:发送凭证头并回显具体 Origin。
|
|
func TestCORSAllowCredentialsExplicitOrigin(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CORSWithConfig(&config.CORSConfig{
|
|
AllowedOrigins: []string{"https://example.com"},
|
|
AllowCredentials: true,
|
|
}))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "https://example.com")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
|
|
t.Errorf("Access-Control-Allow-Credentials = %q, want \"true\"", got)
|
|
}
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
|
t.Errorf("Access-Control-Allow-Origin = %q, want \"https://example.com\"", got)
|
|
}
|
|
if got := w.Header().Get("Vary"); got != "Origin" {
|
|
t.Errorf("Vary = %q, want \"Origin\"", got)
|
|
}
|
|
}
|
|
|
|
// CORS 规范:AllowCredentials=true 时禁止使用 "*",必须回显具体 Origin。
|
|
func TestCORSWildcardWithCredentials(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CORSWithConfig(&config.CORSConfig{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowCredentials: true,
|
|
}))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "https://anywhere.example")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got == "*" {
|
|
t.Errorf("Access-Control-Allow-Origin must NOT be \"*\" when credentials are allowed, got %q", got)
|
|
}
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://anywhere.example" {
|
|
t.Errorf("Access-Control-Allow-Origin = %q, want echoed origin", got)
|
|
}
|
|
if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "true" {
|
|
t.Errorf("Access-Control-Allow-Credentials = %q, want \"true\"", got)
|
|
}
|
|
}
|
|
|
|
// AllowedOrigins=["*"] 且 AllowCredentials=false:保持 "*" 通配符语义。
|
|
func TestCORSWildcardWithoutCredentials(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CORSWithConfig(&config.CORSConfig{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowCredentials: false,
|
|
}))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "https://anywhere.example")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
|
|
t.Errorf("Access-Control-Allow-Origin = %q, want \"*\"", got)
|
|
}
|
|
if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
|
t.Errorf("Access-Control-Allow-Credentials = %q, want empty (AllowCredentials=false)", got)
|
|
}
|
|
}
|
|
|
|
// Origin 不在白名单时不应回显,避免反射型 CORS 漏洞。
|
|
func TestCORSOriginNotAllowed(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CORSWithConfig(&config.CORSConfig{
|
|
AllowedOrigins: []string{"https://allowed.example"},
|
|
AllowCredentials: true,
|
|
}))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Origin", "https://evil.example")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
|
t.Errorf("Access-Control-Allow-Origin = %q, want empty for non-whitelisted origin", got)
|
|
}
|
|
if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
|
t.Errorf("Access-Control-Allow-Credentials = %q, want empty for non-whitelisted origin", got)
|
|
}
|
|
}
|
|
|
|
// ===== RateLimit Tests =====
|
|
|
|
func TestRateLimit(t *testing.T) {
|
|
r := setupTestRouter()
|
|
// 使用自定义限流器
|
|
limiter := middleware.NewRateLimiter(10, time.Minute) // 每分钟10次
|
|
defer limiter.Stop()
|
|
r.Use(middleware.RateLimit(limiter))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// 正常请求应该成功
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// 单次请求应该成功
|
|
if w.Code != 200 {
|
|
t.Errorf("RateLimit status = %d, want 200", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRateLimiterAllow(t *testing.T) {
|
|
limiter := middleware.NewRateLimiter(3, time.Minute) // 每分钟3次
|
|
defer limiter.Stop()
|
|
|
|
// 前3次允许
|
|
for i := 0; i < 3; i++ {
|
|
if !limiter.Allow("192.168.1.1") {
|
|
t.Errorf("Request %d should be allowed", i+1)
|
|
}
|
|
}
|
|
|
|
// 第4次拒绝
|
|
if limiter.Allow("192.168.1.1") {
|
|
t.Error("Request 4 should be denied")
|
|
}
|
|
|
|
// 不同 IP 应该独立计数
|
|
if !limiter.Allow("192.168.1.2") {
|
|
t.Error("Different IP should be allowed")
|
|
}
|
|
}
|
|
|
|
func TestCustomRateLimit(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CustomRateLimit(5, time.Minute))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("CustomRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestLoginRateLimit(t *testing.T) {
|
|
middleware.InitRateLimiters()
|
|
defer middleware.StopRateLimiters()
|
|
|
|
r := setupTestRouter()
|
|
r.Use(middleware.LoginRateLimit())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("LoginRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAPIRateLimit(t *testing.T) {
|
|
middleware.InitRateLimiters()
|
|
defer middleware.StopRateLimiters()
|
|
|
|
r := setupTestRouter()
|
|
r.Use(middleware.APIRateLimit())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("APIRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestUploadRateLimit(t *testing.T) {
|
|
middleware.InitRateLimiters()
|
|
defer middleware.StopRateLimiters()
|
|
|
|
r := setupTestRouter()
|
|
r.Use(middleware.UploadRateLimit())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("UploadRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ===== RedisRateLimiter Tests =====
|
|
|
|
func TestRedisRateLimiter(t *testing.T) {
|
|
limiter := middleware.NewRedisRateLimiter("test_limit", 10, time.Minute)
|
|
|
|
// Without Redis, should always allow
|
|
ctx := context.Background()
|
|
allowed, err := limiter.Allow(ctx, "192.168.1.1")
|
|
if err != nil {
|
|
t.Errorf("RedisRateLimiter error: %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Error("RedisRateLimiter should allow without Redis")
|
|
}
|
|
}
|
|
|
|
func TestRedisRateLimitMiddleware(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.RedisRateLimit("test_api", 100))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Without Redis, should pass
|
|
if w.Code != 200 {
|
|
t.Errorf("RedisRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestLoginRedisRateLimit(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.LoginRedisRateLimit())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("LoginRedisRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAPIRedisRateLimit(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.APIRedisRateLimit())
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("APIRedisRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestCustomRedisRateLimit(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.Use(middleware.CustomRedisRateLimit("custom", 50, time.Minute))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("CustomRedisRateLimit status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRedisRateLimiterGetCount(t *testing.T) {
|
|
limiter := middleware.NewRedisRateLimiter("test_count", 10, time.Minute)
|
|
|
|
ctx := context.Background()
|
|
count, err := limiter.GetCount(ctx, "192.168.1.1")
|
|
if err != nil {
|
|
t.Errorf("GetCount error: %v", err)
|
|
}
|
|
// Without Redis, count should be 0
|
|
if count != 0 {
|
|
t.Errorf("GetCount = %d, want 0 without Redis", count)
|
|
}
|
|
}
|
|
|
|
func TestRedisRateLimiterReset(t *testing.T) {
|
|
limiter := middleware.NewRedisRateLimiter("test_reset", 10, time.Minute)
|
|
|
|
ctx := context.Background()
|
|
err := limiter.Reset(ctx, "192.168.1.1")
|
|
if err != nil {
|
|
t.Errorf("Reset error: %v", err)
|
|
}
|
|
} |