feat: 系统全面优化 - 设备管理/登录日志导出/性能监控/设置页面
后端: - 新增全局设备管理 API(DeviceHandler.GetAllDevices) - 新增登录日志导出功能(LogHandler.ExportLoginLogs, CSV/XLSX) - 新增设置服务(SettingsService)和设置页面 API - 设备管理支持多条件筛选(状态/信任状态/关键词) - 登录日志支持流式导出防 OOM - 操作日志支持按方法/时间范围搜索 - 主题配置服务(ThemeService) - 增强监控健康检查(Prometheus metrics + SLO) - 移除旧 ratelimit.go(已迁移至 robustness) - 修复 SocialAccount NULL 扫描问题 - 新增 API 契约测试、Handler 测试、Settings 测试 前端: - 新增管理员设备管理页面(DevicesPage) - 新增管理员登录日志导出功能 - 新增系统设置页面(SettingsPage) - 设备管理支持筛选和分页 - 增强 HTTP 响应类型 测试: - 业务逻辑测试 68 个(含并发 CONC_001~003) - 规模测试 16 个(P99 百分位统计) - E2E 测试、集成测试、契约测试 - 性能基准测试、鲁棒性测试 全面测试通过(38 个测试包)
This commit is contained in:
308
internal/service/settings_test.go
Normal file
308
internal/service/settings_test.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/user-management-system/internal/api/handler"
|
||||
"github.com/user-management-system/internal/api/middleware"
|
||||
"github.com/user-management-system/internal/api/router"
|
||||
"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/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"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// doRequest makes an HTTP request with optional body
|
||||
func doRequest(method, url string, token string, body interface{}) (*http.Response, string) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
jsonBytes, _ := json.Marshal(body)
|
||||
bodyReader = bytes.NewReader(jsonBytes)
|
||||
}
|
||||
req, _ := http.NewRequest(method, url, bodyReader)
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{}
|
||||
resp, _ := client.Do(req)
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return resp, string(bodyBytes)
|
||||
}
|
||||
|
||||
func doPost(url, token string, body interface{}) (*http.Response, string) {
|
||||
return doRequest("POST", url, token, body)
|
||||
}
|
||||
|
||||
func doGet(url, token string) (*http.Response, string) {
|
||||
return doRequest("GET", url, token, nil)
|
||||
}
|
||||
|
||||
func setupSettingsTestServer(t *testing.T) (*httptest.Server, *service.SettingsService, string, func()) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// 使用内存 SQLite
|
||||
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
||||
DriverName: "sqlite",
|
||||
DSN: "file::memory:?mode=memory&cache=shared",
|
||||
}), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Skipf("skipping test (SQLite unavailable): %v", err)
|
||||
return nil, nil, "", func() {}
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := db.AutoMigrate(
|
||||
&domain.User{},
|
||||
&domain.Role{},
|
||||
&domain.Permission{},
|
||||
&domain.UserRole{},
|
||||
&domain.RolePermission{},
|
||||
&domain.Device{},
|
||||
&domain.LoginLog{},
|
||||
&domain.OperationLog{},
|
||||
&domain.SocialAccount{},
|
||||
&domain.Webhook{},
|
||||
&domain.WebhookDelivery{},
|
||||
); err != nil {
|
||||
t.Fatalf("db migration failed: %v", err)
|
||||
}
|
||||
|
||||
// 创建 JWT Manager
|
||||
jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{
|
||||
HS256Secret: "test-settings-secret-key",
|
||||
AccessTokenExpire: 15 * time.Minute,
|
||||
RefreshTokenExpire: 7 * 24 * time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create jwt manager failed: %v", err)
|
||||
}
|
||||
|
||||
// 创建缓存
|
||||
l1Cache := cache.NewL1Cache()
|
||||
l2Cache := cache.NewRedisCache(false)
|
||||
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
||||
|
||||
// 创建 repositories
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
roleRepo := repository.NewRoleRepository(db)
|
||||
permissionRepo := repository.NewPermissionRepository(db)
|
||||
userRoleRepo := repository.NewUserRoleRepository(db)
|
||||
rolePermissionRepo := repository.NewRolePermissionRepository(db)
|
||||
deviceRepo := repository.NewDeviceRepository(db)
|
||||
loginLogRepo := repository.NewLoginLogRepository(db)
|
||||
opLogRepo := repository.NewOperationLogRepository(db)
|
||||
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db)
|
||||
|
||||
// 创建 services
|
||||
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
||||
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
||||
userSvc := service.NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo)
|
||||
roleSvc := service.NewRoleService(roleRepo, rolePermissionRepo)
|
||||
permSvc := service.NewPermissionService(permissionRepo)
|
||||
deviceSvc := service.NewDeviceService(deviceRepo, userRepo)
|
||||
loginLogSvc := service.NewLoginLogService(loginLogRepo)
|
||||
opLogSvc := service.NewOperationLogService(opLogRepo)
|
||||
|
||||
// 创建 SettingsService
|
||||
settingsService := service.NewSettingsService()
|
||||
|
||||
// 创建 middleware
|
||||
rateLimitCfg := config.RateLimitConfig{}
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg)
|
||||
authMiddleware := middleware.NewAuthMiddleware(
|
||||
jwtManager, userRepo, userRoleRepo, roleRepo, rolePermissionRepo, permissionRepo, l1Cache,
|
||||
)
|
||||
authMiddleware.SetCacheManager(cacheManager)
|
||||
opLogMiddleware := middleware.NewOperationLogMiddleware(opLogRepo)
|
||||
|
||||
// 创建 handlers
|
||||
authHandler := handler.NewAuthHandler(authSvc)
|
||||
userHandler := handler.NewUserHandler(userSvc)
|
||||
roleHandler := handler.NewRoleHandler(roleSvc)
|
||||
permHandler := handler.NewPermissionHandler(permSvc)
|
||||
deviceHandler := handler.NewDeviceHandler(deviceSvc)
|
||||
logHandler := handler.NewLogHandler(loginLogSvc, opLogSvc)
|
||||
settingsHandler := handler.NewSettingsHandler(settingsService)
|
||||
|
||||
// 创建 router - 22个handler参数(含 metrics)+ variadic avatarHandler
|
||||
r := router.NewRouter(
|
||||
authHandler, userHandler, roleHandler, permHandler, deviceHandler,
|
||||
logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware,
|
||||
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
nil,
|
||||
settingsHandler, nil,
|
||||
)
|
||||
engine := r.Setup()
|
||||
|
||||
server := httptest.NewServer(engine)
|
||||
|
||||
// 注册用户用于测试
|
||||
resp, _ := doPost(server.URL+"/api/v1/auth/register", "", map[string]interface{}{
|
||||
"username": "admintestsu",
|
||||
"email": "admintestsu@test.com",
|
||||
"password": "Password123!",
|
||||
})
|
||||
resp.Body.Close()
|
||||
|
||||
// 获取 token
|
||||
loginResp, _ := doPost(server.URL+"/api/v1/auth/login", "", map[string]interface{}{
|
||||
"account": "admintestsu",
|
||||
"password": "Password123!",
|
||||
})
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(loginResp.Body).Decode(&result)
|
||||
loginResp.Body.Close()
|
||||
|
||||
token := ""
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
token, _ = data["access_token"].(string)
|
||||
}
|
||||
|
||||
return server, settingsService, token, func() {
|
||||
server.Close()
|
||||
if sqlDB, _ := db.DB(); sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Settings API Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestGetSettings_Success(t *testing.T) {
|
||||
// 仅测试 service 层,不测试 HTTP API
|
||||
svc := service.NewSettingsService()
|
||||
settings, err := svc.GetSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetSettings failed: %v", err)
|
||||
}
|
||||
|
||||
if settings.System.Name != "用户管理系统" {
|
||||
t.Errorf("expected system name '用户管理系统', got '%s'", settings.System.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSettings_Unauthorized(t *testing.T) {
|
||||
server, _, _, cleanup := setupSettingsTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
req, _ := http.NewRequest("GET", server.URL+"/api/v1/admin/settings", nil)
|
||||
// 不设置 Authorization header
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 无 token 应该返回 401
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSettings_ResponseStructure(t *testing.T) {
|
||||
// 仅测试 service 层数据结构
|
||||
svc := service.NewSettingsService()
|
||||
settings, err := svc.GetSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetSettings failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证 system 字段
|
||||
if settings.System.Name == "" {
|
||||
t.Error("System.Name should not be empty")
|
||||
}
|
||||
if settings.System.Version == "" {
|
||||
t.Error("System.Version should not be empty")
|
||||
}
|
||||
if settings.System.Environment == "" {
|
||||
t.Error("System.Environment should not be empty")
|
||||
}
|
||||
|
||||
// 验证 security 字段
|
||||
if settings.Security.PasswordMinLength == 0 {
|
||||
t.Error("Security.PasswordMinLength should not be zero")
|
||||
}
|
||||
if !settings.Security.PasswordRequireUppercase {
|
||||
t.Error("Security.PasswordRequireUppercase should be true")
|
||||
}
|
||||
|
||||
// 验证 features 字段
|
||||
if !settings.Features.EmailVerification {
|
||||
t.Error("Features.EmailVerification should be true")
|
||||
}
|
||||
if len(settings.Features.OAuthProviders) == 0 {
|
||||
t.Error("Features.OAuthProviders should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SettingsService Unit Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestSettingsService_GetSettings(t *testing.T) {
|
||||
svc := service.NewSettingsService()
|
||||
|
||||
settings, err := svc.GetSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetSettings failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证 system
|
||||
if settings.System.Name == "" {
|
||||
t.Error("System.Name should not be empty")
|
||||
}
|
||||
if settings.System.Version == "" {
|
||||
t.Error("System.Version should not be empty")
|
||||
}
|
||||
|
||||
// 验证 security defaults
|
||||
if settings.Security.PasswordMinLength != 8 {
|
||||
t.Errorf("PasswordMinLength: got %d, want 8", settings.Security.PasswordMinLength)
|
||||
}
|
||||
if !settings.Security.PasswordRequireUppercase {
|
||||
t.Error("PasswordRequireUppercase should be true")
|
||||
}
|
||||
if !settings.Security.PasswordRequireLowercase {
|
||||
t.Error("PasswordRequireLowercase should be true")
|
||||
}
|
||||
if !settings.Security.PasswordRequireNumbers {
|
||||
t.Error("PasswordRequireNumbers should be true")
|
||||
}
|
||||
if !settings.Security.PasswordRequireSymbols {
|
||||
t.Error("PasswordRequireSymbols should be true")
|
||||
}
|
||||
if settings.Security.PasswordHistory != 5 {
|
||||
t.Errorf("PasswordHistory: got %d, want 5", settings.Security.PasswordHistory)
|
||||
}
|
||||
|
||||
// 验证 features defaults
|
||||
if !settings.Features.EmailVerification {
|
||||
t.Error("EmailVerification should be true")
|
||||
}
|
||||
if settings.Features.DataExportEnabled != true {
|
||||
t.Error("DataExportEnabled should be true")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user