309 lines
9.4 KiB
Go
309 lines
9.4 KiB
Go
|
|
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")
|
|||
|
|
}
|
|||
|
|
}
|