- 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)
492 lines
15 KiB
Go
492 lines
15 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/user-management-system/internal/auth"
|
|
"github.com/user-management-system/internal/cache"
|
|
"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"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Auth Capabilities Tests - Phase 1
|
|
// =============================================================================
|
|
|
|
func setupCapabilitiesTestEnv(t *testing.T) (*service.AuthService, *gorm.DB) {
|
|
t.Helper()
|
|
|
|
dsn := "file:cap_test?mode=memory&cache=shared"
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|
DriverName: "sqlite",
|
|
DSN: dsn,
|
|
}), &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.UserRole{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Seed roles
|
|
db.Create(&domain.Role{Code: "admin", Name: "管理员", Status: domain.RoleStatusEnabled})
|
|
db.Create(&domain.Role{Code: "user", Name: "用户", Status: domain.RoleStatusEnabled})
|
|
|
|
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
HS256Secret: "test-secret",
|
|
AccessTokenExpire: 15 * time.Minute,
|
|
RefreshTokenExpire: 7 * 24 * time.Hour,
|
|
})
|
|
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
roleRepo := repository.NewRoleRepository(db)
|
|
userRoleRepo := repository.NewUserRoleRepository(db)
|
|
|
|
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
|
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
|
|
return authSvc, db
|
|
}
|
|
|
|
func TestAuthCapabilities_SimpleMethods(t *testing.T) {
|
|
svc, _ := setupCapabilitiesTestEnv(t)
|
|
ctx := context.Background()
|
|
|
|
t.Run("SupportsEmailActivation", func(t *testing.T) {
|
|
if svc.SupportsEmailActivation() {
|
|
t.Error("Should not support email activation without config")
|
|
}
|
|
})
|
|
|
|
t.Run("SupportsEmailCodeLogin", func(t *testing.T) {
|
|
if svc.SupportsEmailCodeLogin() {
|
|
t.Error("Should not support email code login without config")
|
|
}
|
|
})
|
|
|
|
t.Run("SupportsSMSCodeLogin", func(t *testing.T) {
|
|
if svc.SupportsSMSCodeLogin() {
|
|
t.Error("Should not support SMS code login without config")
|
|
}
|
|
})
|
|
|
|
t.Run("GetAuthCapabilities", func(t *testing.T) {
|
|
caps := svc.GetAuthCapabilities(ctx)
|
|
if !caps.Password {
|
|
t.Error("Password should always be true")
|
|
}
|
|
})
|
|
|
|
t.Run("GetAuthCapabilities with nil ctx", func(t *testing.T) {
|
|
caps := svc.GetAuthCapabilities(nil)
|
|
if !caps.Password {
|
|
t.Error("Password should always be true")
|
|
}
|
|
})
|
|
|
|
t.Run("IsAdminBootstrapRequired with nil ctx", func(t *testing.T) {
|
|
// 测试nil ctx不会panic
|
|
_ = svc.IsAdminBootstrapRequired(nil)
|
|
})
|
|
|
|
t.Run("nil service methods", func(t *testing.T) {
|
|
var nilSvc *service.AuthService
|
|
|
|
if nilSvc.SupportsEmailActivation() {
|
|
t.Error("nil service should return false")
|
|
}
|
|
if nilSvc.SupportsEmailCodeLogin() {
|
|
t.Error("nil service should return false")
|
|
}
|
|
if nilSvc.SupportsSMSCodeLogin() {
|
|
t.Error("nil service should return false")
|
|
}
|
|
if nilSvc.IsAdminBootstrapRequired(ctx) {
|
|
t.Error("nil service should return false")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthCapabilities_IsAdminBootstrapRequired(t *testing.T) {
|
|
svc, _ := setupCapabilitiesTestEnv(t)
|
|
ctx := context.Background()
|
|
|
|
t.Run("Admin bootstrap required when no admin", func(t *testing.T) {
|
|
required := svc.IsAdminBootstrapRequired(ctx)
|
|
// Should be true since no admin user exists
|
|
if !required {
|
|
t.Log("Admin bootstrap should be required when no admin exists")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test nil service behavior
|
|
func TestAuthService_NilBehavior(t *testing.T) {
|
|
ctx := context.Background()
|
|
var nilSvc *service.AuthService
|
|
|
|
t.Run("nil service RefreshToken", func(t *testing.T) {
|
|
_, err := nilSvc.RefreshToken(ctx, "token")
|
|
if err == nil {
|
|
t.Error("nil service should return error")
|
|
}
|
|
})
|
|
|
|
t.Run("nil service GetUserInfo", func(t *testing.T) {
|
|
_, err := nilSvc.GetUserInfo(ctx, 1)
|
|
if err == nil {
|
|
t.Error("nil service should return error")
|
|
}
|
|
})
|
|
|
|
t.Run("nil service Logout", func(t *testing.T) {
|
|
err := nilSvc.Logout(ctx, "user", nil)
|
|
if err != nil {
|
|
t.Errorf("nil service Logout should not error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nil service IsTokenBlacklisted", func(t *testing.T) {
|
|
blacklisted := nilSvc.IsTokenBlacklisted(ctx, "jti")
|
|
if blacklisted {
|
|
t.Error("nil service should return false")
|
|
}
|
|
})
|
|
|
|
t.Run("nil service GetAuthCapabilities", func(t *testing.T) {
|
|
caps := nilSvc.GetAuthCapabilities(ctx)
|
|
// nil service returns empty capabilities, Password is false
|
|
_ = caps
|
|
t.Logf("nil service GetAuthCapabilities: %+v", caps)
|
|
})
|
|
|
|
t.Run("nil service RefreshTokenTTLSeconds", func(t *testing.T) {
|
|
ttl := nilSvc.RefreshTokenTTLSeconds()
|
|
if ttl != 0 {
|
|
t.Errorf("nil service should return 0, got %d", ttl)
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// IsAdminBootstrapRequired Tests
|
|
// =============================================================================
|
|
|
|
func TestAuthService_IsAdminBootstrapRequired(t *testing.T) {
|
|
t.Run("nil service returns false", func(t *testing.T) {
|
|
var nilSvc *service.AuthService
|
|
result := nilSvc.IsAdminBootstrapRequired(context.Background())
|
|
if result {
|
|
t.Error("nil service should return false")
|
|
}
|
|
})
|
|
|
|
t.Run("service without role repo returns false", func(t *testing.T) {
|
|
svc := &service.AuthService{}
|
|
result := svc.IsAdminBootstrapRequired(context.Background())
|
|
if result {
|
|
t.Error("service without role repo should return false")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// IsAdminBootstrapRequired Extended Tests
|
|
// =============================================================================
|
|
|
|
func TestAuthService_IsAdminBootstrapRequired_Extended(t *testing.T) {
|
|
t.Run("returns true when admin role not found", func(t *testing.T) {
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|
DriverName: "sqlite",
|
|
DSN: "file:cap_test_no_role?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.UserRole{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
// Do NOT create admin role
|
|
|
|
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
HS256Secret: "test-secret",
|
|
AccessTokenExpire: 15 * time.Minute,
|
|
RefreshTokenExpire: 7 * 24 * time.Hour,
|
|
})
|
|
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
roleRepo := repository.NewRoleRepository(db)
|
|
userRoleRepo := repository.NewUserRoleRepository(db)
|
|
|
|
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
|
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
|
|
result := authSvc.IsAdminBootstrapRequired(context.Background())
|
|
if !result {
|
|
t.Error("Should return true when admin role not found")
|
|
}
|
|
})
|
|
|
|
t.Run("returns true when admin role exists but no users assigned", func(t *testing.T) {
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|
DriverName: "sqlite",
|
|
DSN: "file:cap_test_no_users?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.UserRole{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create admin role but no users
|
|
db.Create(&domain.Role{Code: "admin", Name: "管理员", Status: domain.RoleStatusEnabled})
|
|
|
|
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
HS256Secret: "test-secret",
|
|
AccessTokenExpire: 15 * time.Minute,
|
|
RefreshTokenExpire: 7 * 24 * time.Hour,
|
|
})
|
|
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
roleRepo := repository.NewRoleRepository(db)
|
|
userRoleRepo := repository.NewUserRoleRepository(db)
|
|
|
|
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
|
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
|
|
result := authSvc.IsAdminBootstrapRequired(context.Background())
|
|
if !result {
|
|
t.Error("Should return true when no admin users assigned")
|
|
}
|
|
})
|
|
|
|
t.Run("returns false when active admin user exists", func(t *testing.T) {
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|
DriverName: "sqlite",
|
|
DSN: "file:cap_test_active_admin?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.UserRole{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create admin role
|
|
adminRole := &domain.Role{Code: "admin", Name: "管理员", Status: domain.RoleStatusEnabled}
|
|
db.Create(adminRole)
|
|
|
|
// Create active admin user
|
|
adminUser := &domain.User{
|
|
Username: "admin",
|
|
Password: "hashed",
|
|
Status: domain.UserStatusActive,
|
|
}
|
|
db.Create(adminUser)
|
|
|
|
// Assign admin role
|
|
db.Create(&domain.UserRole{UserID: adminUser.ID, RoleID: adminRole.ID})
|
|
|
|
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
HS256Secret: "test-secret",
|
|
AccessTokenExpire: 15 * time.Minute,
|
|
RefreshTokenExpire: 7 * 24 * time.Hour,
|
|
})
|
|
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
roleRepo := repository.NewRoleRepository(db)
|
|
userRoleRepo := repository.NewUserRoleRepository(db)
|
|
|
|
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
|
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
|
|
result := authSvc.IsAdminBootstrapRequired(context.Background())
|
|
if result {
|
|
t.Error("Should return false when active admin user exists")
|
|
}
|
|
})
|
|
|
|
t.Run("returns true when admin user is not active", func(t *testing.T) {
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|
DriverName: "sqlite",
|
|
DSN: "file:cap_test_inactive_admin?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.UserRole{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create admin role
|
|
adminRole := &domain.Role{Code: "admin", Name: "管理员", Status: domain.RoleStatusEnabled}
|
|
db.Create(adminRole)
|
|
|
|
// Create inactive admin user
|
|
adminUser := &domain.User{
|
|
Username: "admin",
|
|
Password: "hashed",
|
|
Status: domain.UserStatusInactive,
|
|
}
|
|
db.Create(adminUser)
|
|
|
|
// Assign admin role
|
|
db.Create(&domain.UserRole{UserID: adminUser.ID, RoleID: adminRole.ID})
|
|
|
|
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
HS256Secret: "test-secret",
|
|
AccessTokenExpire: 15 * time.Minute,
|
|
RefreshTokenExpire: 7 * 24 * time.Hour,
|
|
})
|
|
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
roleRepo := repository.NewRoleRepository(db)
|
|
userRoleRepo := repository.NewUserRoleRepository(db)
|
|
|
|
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
|
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
|
|
result := authSvc.IsAdminBootstrapRequired(context.Background())
|
|
if !result {
|
|
t.Error("Should return true when admin user is not active")
|
|
}
|
|
})
|
|
|
|
t.Run("returns true when admin user is locked", func(t *testing.T) {
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|
DriverName: "sqlite",
|
|
DSN: "file:cap_test_locked_admin?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.UserRole{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create admin role
|
|
adminRole := &domain.Role{Code: "admin", Name: "管理员", Status: domain.RoleStatusEnabled}
|
|
db.Create(adminRole)
|
|
|
|
// Create locked admin user
|
|
adminUser := &domain.User{
|
|
Username: "admin",
|
|
Password: "hashed",
|
|
Status: domain.UserStatusLocked,
|
|
}
|
|
db.Create(adminUser)
|
|
|
|
// Assign admin role
|
|
db.Create(&domain.UserRole{UserID: adminUser.ID, RoleID: adminRole.ID})
|
|
|
|
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
HS256Secret: "test-secret",
|
|
AccessTokenExpire: 15 * time.Minute,
|
|
RefreshTokenExpire: 7 * 24 * time.Hour,
|
|
})
|
|
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
roleRepo := repository.NewRoleRepository(db)
|
|
userRoleRepo := repository.NewUserRoleRepository(db)
|
|
|
|
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
|
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
|
|
result := authSvc.IsAdminBootstrapRequired(context.Background())
|
|
if !result {
|
|
t.Error("Should return true when admin user is locked")
|
|
}
|
|
})
|
|
|
|
t.Run("returns true when admin role is disabled", func(t *testing.T) {
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|
DriverName: "sqlite",
|
|
DSN: "file:cap_test_disabled_role?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.UserRole{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create disabled admin role
|
|
adminRole := &domain.Role{Code: "admin", Name: "管理员", Status: domain.RoleStatusDisabled}
|
|
db.Create(adminRole)
|
|
|
|
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
HS256Secret: "test-secret",
|
|
AccessTokenExpire: 15 * time.Minute,
|
|
RefreshTokenExpire: 7 * 24 * time.Hour,
|
|
})
|
|
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
roleRepo := repository.NewRoleRepository(db)
|
|
userRoleRepo := repository.NewUserRoleRepository(db)
|
|
|
|
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
|
|
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
|
|
result := authSvc.IsAdminBootstrapRequired(context.Background())
|
|
if !result {
|
|
t.Error("Should return true when admin role is disabled")
|
|
}
|
|
})
|
|
}
|