197 lines
4.7 KiB
Go
197 lines
4.7 KiB
Go
|
|
package ratelimit
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
)
|
||
|
|
|
||
|
|
// mockLimiter 用于测试的 mock 限流器。
|
||
|
|
type mockLimiter struct {
|
||
|
|
allowFunc func(ctx context.Context, key string) (bool, time.Duration)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockLimiter) Allow(ctx context.Context, key string) (bool, time.Duration) {
|
||
|
|
if m.allowFunc != nil {
|
||
|
|
return m.allowFunc(ctx, key)
|
||
|
|
}
|
||
|
|
return true, 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockLimiter) Stop() {}
|
||
|
|
|
||
|
|
// 编译期接口检查
|
||
|
|
var _ Limiter = (*mockLimiter)(nil)
|
||
|
|
|
||
|
|
func TestMiddleware_Allow(t *testing.T) {
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
limiter := &mockLimiter{
|
||
|
|
allowFunc: func(ctx context.Context, key string) (bool, time.Duration) {
|
||
|
|
return true, 0
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
router := gin.New()
|
||
|
|
router.Use(Middleware(limiter, func(c *gin.Context) string {
|
||
|
|
return "user1:test"
|
||
|
|
}))
|
||
|
|
router.GET("/test", func(c *gin.Context) {
|
||
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
|
|
})
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
||
|
|
|
||
|
|
var resp map[string]interface{}
|
||
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Equal(t, "ok", resp["status"])
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMiddleware_Deny(t *testing.T) {
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
limiter := &mockLimiter{
|
||
|
|
allowFunc: func(ctx context.Context, key string) (bool, time.Duration) {
|
||
|
|
return false, 5 * time.Second
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
router := gin.New()
|
||
|
|
router.Use(Middleware(limiter, func(c *gin.Context) string {
|
||
|
|
return "user1:test"
|
||
|
|
}))
|
||
|
|
router.GET("/test", func(c *gin.Context) {
|
||
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
|
|
})
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
// 验证返回 429
|
||
|
|
assert.Equal(t, http.StatusTooManyRequests, w.Code)
|
||
|
|
|
||
|
|
// 验证 Retry-After header
|
||
|
|
assert.Equal(t, "5", w.Header().Get("Retry-After"))
|
||
|
|
|
||
|
|
// 验证响应体
|
||
|
|
var resp map[string]interface{}
|
||
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Equal(t, "RATE_LIMITED", resp["code"])
|
||
|
|
assert.Contains(t, resp["message"], "retry after")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMiddleware_NilLimiter(t *testing.T) {
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
router := gin.New()
|
||
|
|
router.Use(Middleware(nil, func(c *gin.Context) string {
|
||
|
|
return "user1:test"
|
||
|
|
}))
|
||
|
|
router.GET("/test", func(c *gin.Context) {
|
||
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
|
|
})
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
// nil limiter 应该放行
|
||
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMiddleware_EmptyKey(t *testing.T) {
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
limiter := &mockLimiter{
|
||
|
|
allowFunc: func(ctx context.Context, key string) (bool, time.Duration) {
|
||
|
|
// 不应该被调用
|
||
|
|
t.Error("Allow should not be called with empty key")
|
||
|
|
return false, 0
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
router := gin.New()
|
||
|
|
router.Use(Middleware(limiter, func(c *gin.Context) string {
|
||
|
|
return "" // 返回空 key
|
||
|
|
}))
|
||
|
|
router.GET("/test", func(c *gin.Context) {
|
||
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
|
|
})
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
// 空 key 应该放行
|
||
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMiddleware_KeyFunc(t *testing.T) {
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
var capturedKey string
|
||
|
|
limiter := &mockLimiter{
|
||
|
|
allowFunc: func(ctx context.Context, key string) (bool, time.Duration) {
|
||
|
|
capturedKey = key
|
||
|
|
return true, 0
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
router := gin.New()
|
||
|
|
router.Use(Middleware(limiter, func(c *gin.Context) string {
|
||
|
|
// 从 query 参数提取 user_id
|
||
|
|
userID := c.Query("user_id")
|
||
|
|
return userID + ":test"
|
||
|
|
}))
|
||
|
|
router.GET("/test", func(c *gin.Context) {
|
||
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
|
|
})
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/test?user_id=user123", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
||
|
|
assert.Equal(t, "user123:test", capturedKey)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMiddleware_RetryAfterRounding(t *testing.T) {
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
limiter := &mockLimiter{
|
||
|
|
allowFunc: func(ctx context.Context, key string) (bool, time.Duration) {
|
||
|
|
return false, 2500 * time.Millisecond // 2.5 秒
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
router := gin.New()
|
||
|
|
router.Use(Middleware(limiter, func(c *gin.Context) string {
|
||
|
|
return "user1:test"
|
||
|
|
}))
|
||
|
|
router.GET("/test", func(c *gin.Context) {
|
||
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
|
|
})
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
assert.Equal(t, http.StatusTooManyRequests, w.Code)
|
||
|
|
// 2.5 秒向上取整为 3 秒
|
||
|
|
assert.Equal(t, "3", w.Header().Get("Retry-After"))
|
||
|
|
}
|