test: add comprehensive test coverage and improve code quality
- Add new test files for auth, service, and handler modules - Improve test organization and coverage - Refactor code for better maintainability - Add captcha, settings, stats, and theme handler tests - Add auth module tests (CAS, OAuth, password, SSO, state) - Add service layer tests for auth, export, permissions, roles - All Go tests pass (exit code 0) - All frontend tests pass (325 tests in 59 files)
This commit is contained in:
146
internal/api/handler/captcha_handler_test.go
Normal file
146
internal/api/handler/captcha_handler_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/user-management-system/internal/api/handler"
|
||||
"github.com/user-management-system/internal/cache"
|
||||
"github.com/user-management-system/internal/service"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Captcha Handler Tests - TDD approach
|
||||
// =============================================================================
|
||||
|
||||
func TestCaptchaHandler_GenerateCaptcha(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
l1Cache := cache.NewL1Cache()
|
||||
l2Cache := cache.NewRedisCache(false)
|
||||
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
||||
captchaSvc := service.NewCaptchaService(cacheManager)
|
||||
h := handler.NewCaptchaHandler(captchaSvc)
|
||||
|
||||
t.Run("生成验证码成功", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/captcha/generate", nil)
|
||||
|
||||
h.GenerateCaptcha(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp["code"].(float64) != 0 {
|
||||
t.Errorf("期望 code=0, 得到 %v", resp["code"])
|
||||
}
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
if data["captcha_id"] == "" {
|
||||
t.Error("captcha_id 不应为空")
|
||||
}
|
||||
if data["image"] == "" {
|
||||
t.Error("image 不应为空")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCaptchaHandler_VerifyCaptcha(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
l1Cache := cache.NewL1Cache()
|
||||
l2Cache := cache.NewRedisCache(false)
|
||||
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
||||
captchaSvc := service.NewCaptchaService(cacheManager)
|
||||
h := handler.NewCaptchaHandler(captchaSvc)
|
||||
|
||||
t.Run("验证成功", func(t *testing.T) {
|
||||
// 先生成验证码
|
||||
result, _ := captchaSvc.Generate(nil)
|
||||
// 从缓存获取答案
|
||||
cachedVal, ok := cacheManager.Get(nil, "captcha:"+result.CaptchaID)
|
||||
if !ok {
|
||||
t.Fatal("验证码未存储到缓存")
|
||||
}
|
||||
answer := cachedVal.(string)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
body := `{"captcha_id":"` + result.CaptchaID + `","answer":"` + answer + `"}`
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/captcha/verify", nil)
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader([]byte(body)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.VerifyCaptcha(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("验证失败-错误答案", func(t *testing.T) {
|
||||
result, _ := captchaSvc.Generate(nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
body := `{"captcha_id":"` + result.CaptchaID + `","answer":"wrong"}`
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/captcha/verify", nil)
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader([]byte(body)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.VerifyCaptcha(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("验证失败-缺少参数", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
body := `{"captcha_id":""}`
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/captcha/verify", nil)
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader([]byte(body)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.VerifyCaptcha(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCaptchaHandler_GetCaptchaImage(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
l1Cache := cache.NewL1Cache()
|
||||
l2Cache := cache.NewRedisCache(false)
|
||||
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
||||
captchaSvc := service.NewCaptchaService(cacheManager)
|
||||
h := handler.NewCaptchaHandler(captchaSvc)
|
||||
|
||||
t.Run("获取验证码图片", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/captcha/image?captcha_id=test", nil)
|
||||
|
||||
h.GetCaptchaImage(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -91,8 +91,8 @@ func (h *DeviceHandler) GetMyDevices(c *gin.Context) {
|
||||
"message": "success",
|
||||
"data": gin.H{
|
||||
"items": devices,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
},
|
||||
})
|
||||
@@ -305,8 +305,8 @@ func (h *DeviceHandler) GetUserDevices(c *gin.Context) {
|
||||
"message": "success",
|
||||
"data": gin.H{
|
||||
"items": devices,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
},
|
||||
})
|
||||
@@ -359,8 +359,8 @@ func (h *DeviceHandler) GetAllDevices(c *gin.Context) {
|
||||
"message": "success",
|
||||
"data": gin.H{
|
||||
"items": devices,
|
||||
"total": total,
|
||||
"page": req.Page,
|
||||
"total": total,
|
||||
"page": req.Page,
|
||||
"page_size": req.PageSize,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -107,8 +107,8 @@ func (h *ExportHandler) ImportUsers(c *gin.Context) {
|
||||
"code": 0,
|
||||
"data": gin.H{
|
||||
"success_count": successCount,
|
||||
"fail_count": failCount,
|
||||
"errors": errs,
|
||||
"fail_count": failCount,
|
||||
"errors": errs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
"github.com/user-management-system/internal/auth"
|
||||
"github.com/user-management-system/internal/cache"
|
||||
"github.com/user-management-system/internal/config"
|
||||
"github.com/user-management-system/internal/domain"
|
||||
"github.com/user-management-system/internal/repository"
|
||||
"github.com/user-management-system/internal/service"
|
||||
"github.com/user-management-system/internal/domain"
|
||||
gormsqlite "gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@@ -109,7 +109,7 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) {
|
||||
rateLimitCfg := config.RateLimitConfig{}
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg)
|
||||
authMiddleware := middleware.NewAuthMiddleware(
|
||||
jwtManager, userRepo, userRoleRepo, roleRepo, rolePermissionRepo, permissionRepo, l1Cache,
|
||||
jwtManager, userRepo, userRoleRepo, l1Cache,
|
||||
)
|
||||
authMiddleware.SetCacheManager(cacheManager)
|
||||
opLogMiddleware := middleware.NewOperationLogMiddleware(opLogRepo)
|
||||
@@ -646,10 +646,10 @@ func TestDeviceHandler_CreateDevice_Success(t *testing.T) {
|
||||
token := getToken(server.URL, "createdevice", "UserPass123!")
|
||||
|
||||
resp, body := doPost(server.URL+"/api/v1/devices", token, map[string]interface{}{
|
||||
"name": "My Device",
|
||||
"device_id": "device-001",
|
||||
"device_type": 3, // DeviceTypeDesktop
|
||||
"device_os": "Windows 10",
|
||||
"name": "My Device",
|
||||
"device_id": "device-001",
|
||||
"device_type": 3, // DeviceTypeDesktop
|
||||
"device_os": "Windows 10",
|
||||
"device_browser": "Chrome",
|
||||
})
|
||||
defer resp.Body.Close()
|
||||
|
||||
49
internal/api/handler/settings_handler_test.go
Normal file
49
internal/api/handler/settings_handler_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/user-management-system/internal/api/handler"
|
||||
"github.com/user-management-system/internal/service"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Settings Handler Tests - TDD approach
|
||||
// =============================================================================
|
||||
|
||||
func TestSettingsHandler_GetSettings(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
settingsSvc := service.NewSettingsService()
|
||||
h := handler.NewSettingsHandler(settingsSvc)
|
||||
|
||||
t.Run("获取系统设置成功", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/admin/settings", nil)
|
||||
|
||||
h.GetSettings(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp["code"].(float64) != 0 {
|
||||
t.Errorf("期望 code=0, 得到 %v", resp["code"])
|
||||
}
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
if data["system"] == nil {
|
||||
t.Error("system 不应为空")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -24,13 +24,9 @@ type SMSLoginRequest struct {
|
||||
DeviceOS string `json:"device_os"`
|
||||
}
|
||||
|
||||
// NewSMSHandler creates a new SMSHandler (stub, no SMS configured)
|
||||
func NewSMSHandler() *SMSHandler {
|
||||
return &SMSHandler{}
|
||||
}
|
||||
|
||||
// NewSMSHandlerWithService creates a SMSHandler backed by real AuthService + SMSCodeService
|
||||
func NewSMSHandlerWithService(authService *service.AuthService, smsCodeService *service.SMSCodeService) *SMSHandler {
|
||||
// NewSMSHandler creates a SMSHandler backed by AuthService + SMSCodeService.
|
||||
// If both services are nil, the handler will return 503 for all requests.
|
||||
func NewSMSHandler(authService *service.AuthService, smsCodeService *service.SMSCodeService) *SMSHandler {
|
||||
return &SMSHandler{
|
||||
authService: authService,
|
||||
smsCodeService: smsCodeService,
|
||||
|
||||
@@ -12,25 +12,25 @@ import (
|
||||
|
||||
// SSOHandler SSO 处理程序
|
||||
type SSOHandler struct {
|
||||
ssoManager *auth.SSOManager
|
||||
ssoManager *auth.SSOManager
|
||||
clientsStore auth.SSOClientsStore
|
||||
}
|
||||
|
||||
// NewSSOHandler 创建 SSO 处理程序
|
||||
func NewSSOHandler(ssoManager *auth.SSOManager, clientsStore auth.SSOClientsStore) *SSOHandler {
|
||||
return &SSOHandler{
|
||||
ssoManager: ssoManager,
|
||||
ssoManager: ssoManager,
|
||||
clientsStore: clientsStore,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthorizeRequest 授权请求
|
||||
type AuthorizeRequest struct {
|
||||
ClientID string `form:"client_id" binding:"required"`
|
||||
RedirectURI string `form:"redirect_uri" binding:"required"`
|
||||
ClientID string `form:"client_id" binding:"required"`
|
||||
RedirectURI string `form:"redirect_uri" binding:"required"`
|
||||
ResponseType string `form:"response_type" binding:"required"`
|
||||
Scope string `form:"scope"`
|
||||
State string `form:"state"`
|
||||
Scope string `form:"scope"`
|
||||
State string `form:"state"`
|
||||
}
|
||||
|
||||
// Authorize 处理 SSO 授权请求
|
||||
@@ -220,17 +220,17 @@ func (h *SSOHandler) Token(c *gin.Context) {
|
||||
|
||||
// IntrospectRequest Introspect 请求
|
||||
type IntrospectRequest struct {
|
||||
Token string `form:"token" binding:"required"`
|
||||
Token string `form:"token" binding:"required"`
|
||||
ClientID string `form:"client_id"`
|
||||
}
|
||||
|
||||
// IntrospectResponse Introspect 响应
|
||||
type IntrospectResponse struct {
|
||||
Active bool `json:"active"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
ExpiresAt int64 `json:"exp,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
ExpiresAt int64 `json:"exp,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// Introspect 验证 access token
|
||||
|
||||
113
internal/api/handler/stats_handler_test.go
Normal file
113
internal/api/handler/stats_handler_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/user-management-system/internal/api/handler"
|
||||
"github.com/user-management-system/internal/domain"
|
||||
"github.com/user-management-system/internal/repository"
|
||||
"github.com/user-management-system/internal/service"
|
||||
gormsqlite "gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Stats Handler Tests - TDD approach
|
||||
// =============================================================================
|
||||
|
||||
func setupStatsTestEnv(t *testing.T) (*handler.StatsHandler, *gorm.DB) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
||||
DriverName: "sqlite",
|
||||
DSN: "file:stats_test?mode=memory&cache=shared",
|
||||
}), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect database: %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&domain.User{}, &domain.Role{}, &domain.LoginLog{}); err != nil {
|
||||
t.Fatalf("failed to migrate: %v", err)
|
||||
}
|
||||
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
loginLogRepo := repository.NewLoginLogRepository(db)
|
||||
statsSvc := service.NewStatsService(userRepo, loginLogRepo)
|
||||
|
||||
return handler.NewStatsHandler(statsSvc), db
|
||||
}
|
||||
|
||||
func TestStatsHandler_GetDashboard(t *testing.T) {
|
||||
h, db := setupStatsTestEnv(t)
|
||||
|
||||
// 创建测试用户
|
||||
db.Create(&domain.User{Username: "user1", Status: domain.UserStatusActive})
|
||||
db.Create(&domain.User{Username: "user2", Status: domain.UserStatusInactive})
|
||||
|
||||
t.Run("获取仪表盘统计成功", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/admin/stats/dashboard", nil)
|
||||
|
||||
h.GetDashboard(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp["code"].(float64) != 0 {
|
||||
t.Errorf("期望 code=0, 得到 %v", resp["code"])
|
||||
}
|
||||
|
||||
// data 可能是 map 或 nil
|
||||
if resp["data"] != nil {
|
||||
data := resp["data"].(map[string]interface{})
|
||||
if data["total_users"] == nil {
|
||||
t.Log("total_users 为空,但响应成功")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStatsHandler_GetUserStats(t *testing.T) {
|
||||
h, db := setupStatsTestEnv(t)
|
||||
|
||||
// 创建不同状态的用户
|
||||
db.Create(&domain.User{Username: "active_user", Status: domain.UserStatusActive})
|
||||
db.Create(&domain.User{Username: "inactive_user", Status: domain.UserStatusInactive})
|
||||
db.Create(&domain.User{Username: "locked_user", Status: domain.UserStatusLocked})
|
||||
|
||||
t.Run("获取用户统计成功", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/admin/stats/users", nil)
|
||||
|
||||
h.GetUserStats(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp["code"].(float64) != 0 {
|
||||
t.Errorf("期望 code=0, 得到 %v", resp["code"])
|
||||
}
|
||||
})
|
||||
}
|
||||
137
internal/api/handler/theme_handler_test.go
Normal file
137
internal/api/handler/theme_handler_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/user-management-system/internal/api/handler"
|
||||
"github.com/user-management-system/internal/domain"
|
||||
"github.com/user-management-system/internal/repository"
|
||||
"github.com/user-management-system/internal/service"
|
||||
gormsqlite "gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Theme Handler Tests - TDD approach
|
||||
// =============================================================================
|
||||
|
||||
func setupThemeTestEnv(t *testing.T) (*handler.ThemeHandler, *gorm.DB) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
||||
DriverName: "sqlite",
|
||||
DSN: "file:theme_test?mode=memory&cache=shared",
|
||||
}), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect database: %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&domain.ThemeConfig{}); err != nil {
|
||||
t.Fatalf("failed to migrate: %v", err)
|
||||
}
|
||||
|
||||
themeRepo := repository.NewThemeConfigRepository(db)
|
||||
themeSvc := service.NewThemeService(themeRepo)
|
||||
|
||||
return handler.NewThemeHandler(themeSvc), db
|
||||
}
|
||||
|
||||
func TestThemeHandler_CreateTheme(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
|
||||
t.Run("创建主题成功", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
body := `{"name":"test-theme","primary_color":"#1976d2"}`
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/themes", bytes.NewReader([]byte(body)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.CreateTheme(c)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusCreated, w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp["code"].(float64) != 0 {
|
||||
t.Errorf("期望 code=0, 得到 %v", resp["code"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("创建主题失败-缺少名称", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
body := `{"primary_color":"#1976d2"}`
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/themes", bytes.NewReader([]byte(body)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.CreateTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThemeHandler_ListThemes(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
|
||||
t.Run("获取主题列表", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes", nil)
|
||||
|
||||
h.ListThemes(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThemeHandler_GetTheme(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
|
||||
t.Run("获取主题失败-无效ID", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "invalid"}}
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes/invalid", nil)
|
||||
|
||||
h.GetTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThemeHandler_DeleteTheme(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
|
||||
t.Run("删除主题失败-无效ID", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "invalid"}}
|
||||
c.Request = httptest.NewRequest("DELETE", "/api/v1/themes/invalid", nil)
|
||||
|
||||
h.DeleteTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -515,7 +515,7 @@ func (h *UserHandler) CreateAdmin(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@ func (h *UserHandler) CreateAdmin(c *gin.Context) {
|
||||
adminReq := &service.CreateAdminRequest{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Email: req.Email,
|
||||
Email: req.Email,
|
||||
Nickname: req.Nickname,
|
||||
}
|
||||
|
||||
|
||||
@@ -101,9 +101,12 @@ func setupWebhookTestServer(t *testing.T) (*httptest.Server, *gorm.DB, string, f
|
||||
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
roleRepo := repository.NewRoleRepository(db)
|
||||
_ = roleRepo // kept for future use
|
||||
permissionRepo := repository.NewPermissionRepository(db)
|
||||
_ = permissionRepo
|
||||
userRoleRepo := repository.NewUserRoleRepository(db)
|
||||
rolePermissionRepo := repository.NewRolePermissionRepository(db)
|
||||
_ = rolePermissionRepo
|
||||
|
||||
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
||||
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
||||
@@ -113,7 +116,7 @@ func setupWebhookTestServer(t *testing.T) (*httptest.Server, *gorm.DB, string, f
|
||||
rateLimitCfg := config.RateLimitConfig{}
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg)
|
||||
authMiddleware := middleware.NewAuthMiddleware(
|
||||
jwtManager, userRepo, userRoleRepo, roleRepo, rolePermissionRepo, permissionRepo, l1Cache,
|
||||
jwtManager, userRepo, userRoleRepo, l1Cache,
|
||||
)
|
||||
authMiddleware.SetCacheManager(cacheManager)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
var corsConfig = config.CORSConfig{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowCredentials: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ func ResponseWrapper() gin.HandlerFunc {
|
||||
// 包装 response writer 以捕获输出
|
||||
wrapper := &responseWrapper{
|
||||
ResponseWriter: c.Writer,
|
||||
body: bytes.NewBuffer(nil),
|
||||
statusCode: http.StatusOK,
|
||||
body: bytes.NewBuffer(nil),
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
c.Writer = wrapper
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
"github.com/swaggo/gin-swagger"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
"github.com/user-management-system/internal/api/handler"
|
||||
"github.com/user-management-system/internal/api/middleware"
|
||||
@@ -33,9 +33,9 @@ type Router struct {
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware
|
||||
opLogMiddleware *middleware.OperationLogMiddleware
|
||||
ipFilterMiddleware *middleware.IPFilterMiddleware
|
||||
ssoHandler *handler.SSOHandler
|
||||
settingsHandler *handler.SettingsHandler
|
||||
metrics *monitoring.Metrics // CRIT-01/02: Prometheus 指标
|
||||
ssoHandler *handler.SSOHandler
|
||||
settingsHandler *handler.SettingsHandler
|
||||
metrics *monitoring.Metrics // CRIT-01/02: Prometheus 指标
|
||||
}
|
||||
|
||||
func NewRouter(
|
||||
@@ -86,20 +86,20 @@ func NewRouter(
|
||||
smsHandler: smsHandler,
|
||||
customFieldHandler: customFieldHandler,
|
||||
themeHandler: themeHandler,
|
||||
ssoHandler: ssoHandler,
|
||||
settingsHandler: settingsHandler,
|
||||
ssoHandler: ssoHandler,
|
||||
settingsHandler: settingsHandler,
|
||||
avatarHandler: avatar,
|
||||
authMiddleware: authMiddleware,
|
||||
rateLimitMiddleware: rateLimitMiddleware,
|
||||
opLogMiddleware: opLogMiddleware,
|
||||
ipFilterMiddleware: ipFilterMiddleware,
|
||||
metrics: metrics,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Setup() *gin.Engine {
|
||||
r.engine.Use(middleware.Recover())
|
||||
r.engine.Use(middleware.TraceID()) // 可观察性补强:每个请求生成唯一 trace_id
|
||||
r.engine.Use(middleware.TraceID()) // 可观察性补强:每个请求生成唯一 trace_id
|
||||
r.engine.Use(middleware.ErrorHandler())
|
||||
r.engine.Use(middleware.Logger())
|
||||
r.engine.Use(middleware.SecurityHeaders())
|
||||
|
||||
Reference in New Issue
Block a user