Files
user-system/internal/service/auth_email_test.go
long-agent 3f3bb82f1d fix: v6 code review P0 auth/IDOR fixes + frontend regression patches
Backend fixes:
- auth_handler: P0 认证逻辑修复
- ratelimit: 限速中间件增强 + 新增单元测试
- auth_service: 认证服务逻辑完善 + 新增测试
- server: server 配置增强 + 新增测试
- handler_test: 新增 handler 层集成测试
- auth_bootstrap_test: bootstrap 路径测试

Frontend patches:
- LoginPage/RegisterPage: CSRF + 表单交互修复
- BootstrapAdminPage: 引导流程修复
- DevicesPage: 设备管理页修复
- auth/social-accounts/users/webhooks services: 类型修正
- csrf.ts: CSRF token 处理修正
- E2E 脚本: CDP smoke + auth e2e 增强

Docs:
- FULL_CODE_REVIEW_REPORT_2026-04-20
- report-v6 执行计划
- REAL_PROJECT_STATUS 更新
- .gitignore: 新增 .gocache-*/config.yaml 排除

验证: go build/vet 0错误, go test 42/42 PASS, 0 FAIL
2026-04-23 07:14:12 +08:00

514 lines
16 KiB
Go

package service_test
import (
"context"
"fmt"
"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 Email Service Tests
// =============================================================================
func setupAuthEmailTestEnv(t *testing.T) (*service.AuthService, *gorm.DB) {
t.Helper()
dsn := fmt.Sprintf("file:auth_email_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
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)
}
// Create predefined roles
for _, role := range domain.PredefinedRoles {
db.Create(&role)
}
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: fmt.Sprintf("test-secret-%d", time.Now().UnixNano()),
AccessTokenExpire: 15 * time.Minute,
RefreshTokenExpire: 7 * 24 * time.Hour,
})
userRepo := repository.NewUserRepository(db)
userRoleRepo := repository.NewUserRoleRepository(db)
roleRepo := repository.NewRoleRepository(db)
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
svc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
svc.SetRoleRepositories(userRoleRepo, roleRepo)
return svc, db
}
func TestAuthService_SetEmailActivationService(t *testing.T) {
svc, _ := setupAuthEmailTestEnv(t)
t.Run("Set email activation service", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
emailActivationSvc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
svc.SetEmailActivationService(emailActivationSvc)
// No error means success
})
}
func TestAuthService_SetEmailCodeService(t *testing.T) {
svc, _ := setupAuthEmailTestEnv(t)
t.Run("Set email code service", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
cfg := service.DefaultEmailCodeConfig()
emailCodeSvc := service.NewEmailCodeService(provider, cacheManager, cfg)
svc.SetEmailCodeService(emailCodeSvc)
// No error means success
})
}
func TestAuthService_HasEmailCodeService(t *testing.T) {
svc, _ := setupAuthEmailTestEnv(t)
t.Run("Has email code service false", func(t *testing.T) {
if svc.HasEmailCodeService() {
t.Error("Expected false for service without email code service")
}
})
t.Run("Has email code service true", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
cfg := service.DefaultEmailCodeConfig()
emailCodeSvc := service.NewEmailCodeService(provider, cacheManager, cfg)
svc.SetEmailCodeService(emailCodeSvc)
if !svc.HasEmailCodeService() {
t.Error("Expected true after setting email code service")
}
})
t.Run("Has email code service nil", func(t *testing.T) {
var nilSvc *service.AuthService
if nilSvc.HasEmailCodeService() {
t.Error("Expected false for nil service")
}
})
}
func TestAuthService_SendEmailLoginCode(t *testing.T) {
svc, db := setupAuthEmailTestEnv(t)
ctx := context.Background()
// Create test user with email
email := "logincode@test.com"
user := &domain.User{
Username: "logincodeuser",
Email: &email,
Status: domain.UserStatusActive,
}
db.Create(user)
t.Run("Send email login code without service configured", func(t *testing.T) {
err := svc.SendEmailLoginCode(ctx, "test@test.com")
if err == nil {
t.Error("Expected error when email code service not configured")
}
})
t.Run("Send email login code with service", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
cfg := service.DefaultEmailCodeConfig()
emailCodeSvc := service.NewEmailCodeService(provider, cacheManager, cfg)
svc.SetEmailCodeService(emailCodeSvc)
err := svc.SendEmailLoginCode(ctx, email)
if err != nil {
t.Fatalf("SendEmailLoginCode failed: %v", err)
}
})
t.Run("Send email login code for non-existent email", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
cfg := service.DefaultEmailCodeConfig()
emailCodeSvc := service.NewEmailCodeService(provider, cacheManager, cfg)
svc.SetEmailCodeService(emailCodeSvc)
// Should return nil to avoid user enumeration
err := svc.SendEmailLoginCode(ctx, "nonexistent@test.com")
if err != nil {
t.Fatalf("Expected nil for non-existent email, got: %v", err)
}
})
}
func TestAuthService_LoginByEmailCode(t *testing.T) {
svc, db := setupAuthEmailTestEnv(t)
ctx := context.Background()
// Create test user with email
email := "emailcode@test.com"
user := &domain.User{
Username: "emailcodeuser",
Email: &email,
Status: domain.UserStatusActive,
}
db.Create(user)
t.Run("Login by email code without service", func(t *testing.T) {
_, err := svc.LoginByEmailCode(ctx, email, "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error when email code service not configured")
}
})
t.Run("Login by email code with invalid code", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
cfg := service.DefaultEmailCodeConfig()
emailCodeSvc := service.NewEmailCodeService(provider, cacheManager, cfg)
svc.SetEmailCodeService(emailCodeSvc)
_, err := svc.LoginByEmailCode(ctx, email, "invalid", "127.0.0.1")
if err == nil {
t.Error("Expected error for invalid code")
}
})
}
func TestAuthService_ActivateEmail(t *testing.T) {
svc, db := setupAuthEmailTestEnv(t)
ctx := context.Background()
t.Run("Activate email without service", func(t *testing.T) {
err := svc.ActivateEmail(ctx, "token")
if err == nil {
t.Error("Expected error when email activation service not configured")
}
})
t.Run("Activate email with invalid token", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
emailActivationSvc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
svc.SetEmailActivationService(emailActivationSvc)
err := svc.ActivateEmail(ctx, "invalid_token")
if err == nil {
t.Error("Expected error for invalid token")
}
})
t.Run("Activate email for already active user", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
emailActivationSvc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
svc.SetEmailActivationService(emailActivationSvc)
// Create inactive user and send activation
email := "activate@test.com"
user := &domain.User{
Username: "activateuser",
Email: &email,
Status: domain.UserStatusActive,
}
db.Create(user)
// Manually store a token in cache
cacheManager.Set(ctx, "email_activation:test_token_active", user.ID, 24*60*60*1000000000, 24*60*60*1000000000)
err := svc.ActivateEmail(ctx, "test_token_active")
if err == nil {
t.Error("Expected error for already active user")
}
})
}
func TestAuthService_ResendActivationEmail(t *testing.T) {
svc, db := setupAuthEmailTestEnv(t)
ctx := context.Background()
t.Run("Resend activation without service", func(t *testing.T) {
err := svc.ResendActivationEmail(ctx, "test@test.com")
if err == nil {
t.Error("Expected error when email activation service not configured")
}
})
t.Run("Resend activation for non-existent email", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
emailActivationSvc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
svc.SetEmailActivationService(emailActivationSvc)
// Should return nil to avoid user enumeration
err := svc.ResendActivationEmail(ctx, "nonexistent@test.com")
if err != nil {
t.Errorf("Expected nil for non-existent email, got: %v", err)
}
})
t.Run("Resend activation for active user", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
emailActivationSvc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
svc.SetEmailActivationService(emailActivationSvc)
email := "resendactive@test.com"
user := &domain.User{
Username: "resendactiveuser",
Email: &email,
Status: domain.UserStatusActive,
}
db.Create(user)
// Should return nil for active user
err := svc.ResendActivationEmail(ctx, email)
if err != nil {
t.Errorf("Expected nil for active user, got: %v", err)
}
})
t.Run("Resend activation for inactive user", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
provider := &service.MockEmailProvider{}
emailActivationSvc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
svc.SetEmailActivationService(emailActivationSvc)
email := "resendinactive@test.com"
user := &domain.User{
Username: "resendinactiveuser",
Email: &email,
Status: domain.UserStatusInactive,
}
db.Create(user)
err := svc.ResendActivationEmail(ctx, email)
if err != nil {
t.Fatalf("ResendActivationEmail failed: %v", err)
}
})
}
func TestAuthService_RegisterWithActivation(t *testing.T) {
svc, _ := setupAuthEmailTestEnv(t)
ctx := context.Background()
t.Run("Register with activation success", func(t *testing.T) {
req := &service.RegisterRequest{
Username: "regactuser",
Password: "Password123!",
Email: "regact@test.com",
}
userInfo, err := svc.RegisterWithActivation(ctx, req)
if err != nil {
t.Fatalf("RegisterWithActivation failed: %v", err)
}
if userInfo == nil {
t.Error("Expected user info")
}
})
t.Run("Register with weak password", func(t *testing.T) {
req := &service.RegisterRequest{
Username: "weakpwduser",
Password: "123",
}
_, err := svc.RegisterWithActivation(ctx, req)
if err == nil {
t.Error("Expected error for weak password")
}
})
t.Run("Register with duplicate username", func(t *testing.T) {
req := &service.RegisterRequest{
Username: "regactuser", // Already exists
Password: "Password123!",
}
_, err := svc.RegisterWithActivation(ctx, req)
if err == nil {
t.Error("Expected error for duplicate username")
}
})
}
func TestAuthService_Register_UsesEmailActivationFlowWhenConfigured(t *testing.T) {
svc, db := setupAuthEmailTestEnv(t)
ctx := context.Background()
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
emailActivationSvc := service.NewEmailActivationService(
&service.MockEmailProvider{},
cacheManager,
"http://localhost:8080",
"TestSite",
)
svc.SetEmailActivationService(emailActivationSvc)
userInfo, err := svc.Register(ctx, &service.RegisterRequest{
Username: "register_activation_enabled",
Password: "Password123!",
Email: "register-activation-enabled@test.com",
})
if err != nil {
t.Fatalf("Register failed: %v", err)
}
if userInfo == nil {
t.Fatal("Register returned nil user info")
}
if userInfo.Status != domain.UserStatusInactive {
t.Fatalf("Register status = %d, want %d", userInfo.Status, domain.UserStatusInactive)
}
if userInfo.Nickname != "register_activation_enabled" {
t.Fatalf("Register nickname = %q, want %q", userInfo.Nickname, "register_activation_enabled")
}
var storedUser domain.User
if err := db.WithContext(ctx).Where("username = ?", "register_activation_enabled").First(&storedUser).Error; err != nil {
t.Fatalf("load stored user: %v", err)
}
if storedUser.Status != domain.UserStatusInactive {
t.Fatalf("stored user status = %d, want %d", storedUser.Status, domain.UserStatusInactive)
}
if storedUser.Nickname != "register_activation_enabled" {
t.Fatalf("stored user nickname = %q, want %q", storedUser.Nickname, "register_activation_enabled")
}
}
// =============================================================================
// Login By Email Code Extended Tests
// =============================================================================
func TestAuthService_LoginByEmailCode_Extended(t *testing.T) {
svc, _ := setupAuthEmailTestEnv(t)
ctx := context.Background()
t.Run("LoginByEmailCode without email code service", func(t *testing.T) {
_, err := svc.LoginByEmailCode(ctx, "test@example.com", "code123", "127.0.0.1")
if err == nil {
t.Error("Expected error when email code service not configured")
}
})
t.Run("LoginByEmailCode with empty email", func(t *testing.T) {
// Create a service with email code service
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
emailProvider := &service.MockEmailProvider{}
emailCodeSvc := service.NewEmailCodeService(emailProvider, cacheManager, service.DefaultEmailCodeConfig())
svc.SetEmailCodeService(emailCodeSvc)
_, err := svc.LoginByEmailCode(ctx, "", "code123", "127.0.0.1")
if err == nil {
t.Error("Expected error for empty email")
}
})
t.Run("LoginByEmailCode for non-existent user", func(t *testing.T) {
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
emailProvider := &service.MockEmailProvider{}
emailCodeSvc := service.NewEmailCodeService(emailProvider, cacheManager, service.DefaultEmailCodeConfig())
svc.SetEmailCodeService(emailCodeSvc)
// Store a valid code
cacheManager.Set(ctx, fmt.Sprintf("email_code:login:%s", "nonexistent@test.com"), "123456", time.Minute*5, time.Minute*5)
_, err := svc.LoginByEmailCode(ctx, "nonexistent@test.com", "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error for non-existent user")
}
})
}
// =============================================================================
// Register With Activation Extended Tests
// =============================================================================
func TestAuthService_RegisterWithActivation_Extended(t *testing.T) {
svc, _ := setupAuthEmailTestEnv(t)
ctx := context.Background()
t.Run("Register with duplicate email", func(t *testing.T) {
// Create first user
req1 := &service.RegisterRequest{
Username: "dupemailuser1",
Password: "Password123!",
Email: "dup@test.com",
}
svc.RegisterWithActivation(ctx, req1)
// Try to register with same email
req2 := &service.RegisterRequest{
Username: "dupemailuser2",
Password: "Password123!",
Email: "dup@test.com",
}
_, err := svc.RegisterWithActivation(ctx, req2)
if err == nil {
t.Error("Expected error for duplicate email")
}
})
t.Run("Register with phone", func(t *testing.T) {
phone := "13800138000"
req := &service.RegisterRequest{
Username: "phoneuser",
Password: "Password123!",
Phone: phone,
}
_, err := svc.RegisterWithActivation(ctx, req)
// Phone registration requires SMS verification which is not configured
if err == nil {
t.Error("Expected error for phone registration without SMS service")
}
})
}