Backend fixes: - auth_handler: P0 认证逻辑修复 - ratelimit: 限速中间件增强 + 新增单元测试 - auth_service: 认证服务逻辑完善 + 新增测试 - server: server 配置增强 + 新增测试 - handler_test: 新增 handler 层集成测试 - auth_bootstrap_test: bootstrap 路径测试 Frontend patches: - LoginPage/RegisterPage: CSRF + 表单交互修复 - BootstrapAdminPage: 引导流程修复 - DevicesPage: 设备管理页修复 - auth/social-accounts/users/webhooks services: 类型修正 - csrf.ts: CSRF token 处理修正 - E2E 脚本: CDP smoke + auth e2e 增强 Docs: - FULL_CODE_REVIEW_REPORT_2026-04-20 - report-v6 执行计划 - REAL_PROJECT_STATUS 更新 - .gitignore: 新增 .gocache-*/config.yaml 排除 验证: go build/vet 0错误, go test 42/42 PASS, 0 FAIL
141 lines
4.9 KiB
Go
141 lines
4.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/user-management-system/internal/config"
|
|
)
|
|
|
|
func performRateLimitedRequest(router *gin.Engine, path string, userID int64) *httptest.ResponseRecorder {
|
|
recorder := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
req.Header.Set("X-Test-User-ID", strconv.FormatInt(userID, 10))
|
|
router.ServeHTTP(recorder, req)
|
|
return recorder
|
|
}
|
|
|
|
func performRefreshRateLimitedRequestWithCookie(router *gin.Engine, refreshToken string) *httptest.ResponseRecorder {
|
|
recorder := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/auth/refresh", nil)
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
if refreshToken != "" {
|
|
req.AddCookie(&http.Cookie{Name: "ums_refresh_token", Value: refreshToken})
|
|
}
|
|
router.ServeHTTP(recorder, req)
|
|
return recorder
|
|
}
|
|
|
|
func performRefreshRateLimitedRequestWithBody(router *gin.Engine, refreshToken string) *httptest.ResponseRecorder {
|
|
recorder := httptest.NewRecorder()
|
|
body := bytes.NewBufferString(`{"refresh_token":"` + refreshToken + `"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/auth/refresh", body)
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(recorder, req)
|
|
return recorder
|
|
}
|
|
|
|
func TestRateLimitMiddleware_API_ScopesBudgetByRouteForAuthenticatedUser(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
rateLimitMiddleware := NewRateLimitMiddleware(config.RateLimitConfig{})
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
rawUserID := c.GetHeader("X-Test-User-ID")
|
|
if rawUserID != "" {
|
|
userID, err := strconv.ParseInt(rawUserID, 10, 64)
|
|
if err == nil {
|
|
c.Set("user_id", userID)
|
|
}
|
|
}
|
|
c.Next()
|
|
})
|
|
|
|
protected := router.Group("")
|
|
protected.Use(rateLimitMiddleware.API())
|
|
protected.GET("/users", func(c *gin.Context) {
|
|
c.Status(http.StatusOK)
|
|
})
|
|
protected.GET("/roles", func(c *gin.Context) {
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
for i := 0; i < 100; i++ {
|
|
recorder := performRateLimitedRequest(router, "/users", 1)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("request %d to /users returned %d, want %d", i+1, recorder.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
sameRouteOverflow := performRateLimitedRequest(router, "/users", 1)
|
|
if sameRouteOverflow.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("overflow request to /users returned %d, want %d", sameRouteOverflow.Code, http.StatusTooManyRequests)
|
|
}
|
|
|
|
differentRoute := performRateLimitedRequest(router, "/roles", 1)
|
|
if differentRoute.Code != http.StatusOK {
|
|
t.Fatalf("request to /roles after exhausting /users budget returned %d, want %d", differentRoute.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitMiddleware_Refresh_ScopesBudgetByRefreshCookie(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
rateLimitMiddleware := NewRateLimitMiddleware(config.RateLimitConfig{})
|
|
router := gin.New()
|
|
router.POST("/auth/refresh", rateLimitMiddleware.Refresh(), func(c *gin.Context) {
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
for i := 0; i < 10; i++ {
|
|
recorder := performRefreshRateLimitedRequestWithCookie(router, "refresh-token-a")
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("request %d for refresh-token-a returned %d, want %d", i+1, recorder.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
sameTokenOverflow := performRefreshRateLimitedRequestWithCookie(router, "refresh-token-a")
|
|
if sameTokenOverflow.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("overflow request for refresh-token-a returned %d, want %d", sameTokenOverflow.Code, http.StatusTooManyRequests)
|
|
}
|
|
|
|
differentToken := performRefreshRateLimitedRequestWithCookie(router, "refresh-token-b")
|
|
if differentToken.Code != http.StatusOK {
|
|
t.Fatalf("request for refresh-token-b after exhausting refresh-token-a budget returned %d, want %d", differentToken.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitMiddleware_Refresh_ScopesBudgetByRefreshTokenBody(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
rateLimitMiddleware := NewRateLimitMiddleware(config.RateLimitConfig{})
|
|
router := gin.New()
|
|
router.POST("/auth/refresh", rateLimitMiddleware.Refresh(), func(c *gin.Context) {
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
for i := 0; i < 10; i++ {
|
|
recorder := performRefreshRateLimitedRequestWithBody(router, "refresh-token-a")
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("request %d for refresh-token-a body returned %d, want %d", i+1, recorder.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
sameTokenOverflow := performRefreshRateLimitedRequestWithBody(router, "refresh-token-a")
|
|
if sameTokenOverflow.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("overflow request for refresh-token-a body returned %d, want %d", sameTokenOverflow.Code, http.StatusTooManyRequests)
|
|
}
|
|
|
|
differentToken := performRefreshRateLimitedRequestWithBody(router, "refresh-token-b")
|
|
if differentToken.Code != http.StatusOK {
|
|
t.Fatalf("request for refresh-token-b body after exhausting refresh-token-a budget returned %d, want %d", differentToken.Code, http.StatusOK)
|
|
}
|
|
}
|