Files
user-system/internal/service/settings_test.go
long-agent 5ca3633be4 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 个测试包)
2026-04-07 12:08:16 +08:00

309 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}