- 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)
503 lines
15 KiB
Go
503 lines
15 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/user-management-system/internal/cache"
|
|
"github.com/user-management-system/internal/service"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Captcha Service Tests - Phase 1
|
|
// =============================================================================
|
|
|
|
func TestCaptchaService_Generate(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
svc := service.NewCaptchaService(cacheManager)
|
|
ctx := context.Background()
|
|
|
|
t.Run("Generate captcha", func(t *testing.T) {
|
|
result, err := svc.Generate(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Generate failed: %v", err)
|
|
}
|
|
if result.CaptchaID == "" {
|
|
t.Error("Expected captcha ID")
|
|
}
|
|
if len(result.ImageData) == 0 {
|
|
t.Error("Expected image data")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCaptchaService_Verify(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
svc := service.NewCaptchaService(cacheManager)
|
|
ctx := context.Background()
|
|
|
|
t.Run("Verify captcha success", func(t *testing.T) {
|
|
result, _ := svc.Generate(ctx)
|
|
// Get the answer from cache
|
|
val, ok := cacheManager.Get(ctx, "captcha:"+result.CaptchaID)
|
|
if !ok {
|
|
t.Fatal("Captcha not found in cache")
|
|
}
|
|
answer := val.(string)
|
|
|
|
valid := svc.Verify(ctx, result.CaptchaID, answer)
|
|
if !valid {
|
|
t.Error("Expected captcha to be valid")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify captcha with wrong answer", func(t *testing.T) {
|
|
result, _ := svc.Generate(ctx)
|
|
valid := svc.Verify(ctx, result.CaptchaID, "wrong")
|
|
if valid {
|
|
t.Error("Expected captcha to be invalid")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify captcha with empty ID", func(t *testing.T) {
|
|
valid := svc.Verify(ctx, "", "answer")
|
|
if valid {
|
|
t.Error("Expected false for empty ID")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify captcha with empty answer", func(t *testing.T) {
|
|
result, _ := svc.Generate(ctx)
|
|
valid := svc.Verify(ctx, result.CaptchaID, "")
|
|
if valid {
|
|
t.Error("Expected false for empty answer")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify captcha twice (one-time use)", func(t *testing.T) {
|
|
result, _ := svc.Generate(ctx)
|
|
val, _ := cacheManager.Get(ctx, "captcha:"+result.CaptchaID)
|
|
answer := val.(string)
|
|
|
|
// First verify
|
|
svc.Verify(ctx, result.CaptchaID, answer)
|
|
// Second verify should fail
|
|
valid := svc.Verify(ctx, result.CaptchaID, answer)
|
|
if valid {
|
|
t.Error("Expected captcha to be invalid after first use")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify captcha case insensitive", func(t *testing.T) {
|
|
result, _ := svc.Generate(ctx)
|
|
val, _ := cacheManager.Get(ctx, "captcha:"+result.CaptchaID)
|
|
answer := val.(string)
|
|
|
|
// Verify with uppercase
|
|
valid := svc.Verify(ctx, result.CaptchaID, strings.ToUpper(answer))
|
|
if !valid {
|
|
t.Error("Expected case-insensitive verification")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCaptchaService_ValidateCaptcha(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
svc := service.NewCaptchaService(cacheManager)
|
|
ctx := context.Background()
|
|
|
|
t.Run("ValidateCaptcha with empty ID", func(t *testing.T) {
|
|
err := svc.ValidateCaptcha(ctx, "", "answer")
|
|
if err == nil {
|
|
t.Error("Expected error for empty ID")
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateCaptcha with empty answer", func(t *testing.T) {
|
|
err := svc.ValidateCaptcha(ctx, "id", "")
|
|
if err == nil {
|
|
t.Error("Expected error for empty answer")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCaptchaService_VerifyWithoutDelete(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
svc := service.NewCaptchaService(cacheManager)
|
|
ctx := context.Background()
|
|
|
|
t.Run("VerifyWithoutDelete success", func(t *testing.T) {
|
|
result, _ := svc.Generate(ctx)
|
|
val, _ := cacheManager.Get(ctx, "captcha:"+result.CaptchaID)
|
|
answer := val.(string)
|
|
|
|
valid := svc.VerifyWithoutDelete(ctx, result.CaptchaID, answer)
|
|
if !valid {
|
|
t.Error("Expected captcha to be valid")
|
|
}
|
|
|
|
// Should still be valid after VerifyWithoutDelete
|
|
valid2 := svc.VerifyWithoutDelete(ctx, result.CaptchaID, answer)
|
|
if !valid2 {
|
|
t.Error("Expected captcha to still be valid after VerifyWithoutDelete")
|
|
}
|
|
})
|
|
|
|
t.Run("VerifyWithoutDelete with wrong answer", func(t *testing.T) {
|
|
result, _ := svc.Generate(ctx)
|
|
valid := svc.VerifyWithoutDelete(ctx, result.CaptchaID, "wrong")
|
|
if valid {
|
|
t.Error("Expected captcha to be invalid")
|
|
}
|
|
})
|
|
|
|
t.Run("VerifyWithoutDelete with empty ID", func(t *testing.T) {
|
|
valid := svc.VerifyWithoutDelete(ctx, "", "answer")
|
|
if valid {
|
|
t.Error("Expected false for empty ID")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Settings Service Tests - Phase 1
|
|
// =============================================================================
|
|
|
|
func TestSettingsService_GetSettings(t *testing.T) {
|
|
svc := service.NewSettingsService()
|
|
ctx := context.Background()
|
|
|
|
t.Run("GetSettings returns default values", func(t *testing.T) {
|
|
settings, err := svc.GetSettings(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetSettings failed: %v", err)
|
|
}
|
|
if settings.System.Name == "" {
|
|
t.Error("Expected system name")
|
|
}
|
|
if settings.System.Version == "" {
|
|
t.Error("Expected system version")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Email Code Service Tests - Phase 1
|
|
// =============================================================================
|
|
|
|
func TestEmailCodeService_New(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
t.Run("NewEmailCodeService", func(t *testing.T) {
|
|
cfg := service.DefaultEmailCodeConfig()
|
|
svc := service.NewEmailCodeService(nil, cacheManager, cfg)
|
|
if svc == nil {
|
|
t.Error("Expected service instance")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// SMS Code Service Tests - Phase 1
|
|
// =============================================================================
|
|
|
|
func TestSMSCodeService_New(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
t.Run("NewSMSCodeService", func(t *testing.T) {
|
|
cfg := service.DefaultSMSCodeConfig()
|
|
svc := service.NewSMSCodeService(nil, cacheManager, cfg)
|
|
if svc == nil {
|
|
t.Error("Expected service instance")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Password Reset Service Tests - Phase 1
|
|
// =============================================================================
|
|
|
|
func TestPasswordResetService_New(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
|
|
t.Run("NewPasswordResetService", func(t *testing.T) {
|
|
cfg := service.DefaultPasswordResetConfig()
|
|
svc := service.NewPasswordResetService(nil, cacheManager, cfg)
|
|
if svc == nil {
|
|
t.Error("Expected service instance")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Email Code Service Tests - Extended
|
|
// =============================================================================
|
|
|
|
func TestEmailCodeService_SendEmailCode(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
provider := &service.MockEmailProvider{}
|
|
cfg := service.DefaultEmailCodeConfig()
|
|
svc := service.NewEmailCodeService(provider, cacheManager, cfg)
|
|
ctx := context.Background()
|
|
|
|
t.Run("Send email code success", func(t *testing.T) {
|
|
err := svc.SendEmailCode(ctx, "test@example.com", "login")
|
|
if err != nil {
|
|
t.Fatalf("SendEmailCode failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Send email code with cooldown", func(t *testing.T) {
|
|
// First request
|
|
svc.SendEmailCode(ctx, "cooldown@example.com", "login")
|
|
// Second request should hit cooldown
|
|
err := svc.SendEmailCode(ctx, "cooldown@example.com", "login")
|
|
if err == nil {
|
|
t.Error("Expected rate limit error due to cooldown")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEmailCodeService_VerifyEmailCode(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
provider := &service.MockEmailProvider{}
|
|
cfg := service.DefaultEmailCodeConfig()
|
|
svc := service.NewEmailCodeService(provider, cacheManager, cfg)
|
|
ctx := context.Background()
|
|
|
|
t.Run("Verify email code success", func(t *testing.T) {
|
|
email := "verify@example.com"
|
|
svc.SendEmailCode(ctx, email, "login")
|
|
// Get the code from cache
|
|
val, ok := cacheManager.Get(ctx, "email_code:login:"+email)
|
|
if !ok {
|
|
t.Fatal("Code not found in cache")
|
|
}
|
|
code := val.(string)
|
|
err := svc.VerifyEmailCode(ctx, email, "login", code)
|
|
if err != nil {
|
|
t.Fatalf("VerifyEmailCode failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Verify email code with wrong code", func(t *testing.T) {
|
|
email := "wrong@example.com"
|
|
svc.SendEmailCode(ctx, email, "login")
|
|
err := svc.VerifyEmailCode(ctx, email, "login", "000000")
|
|
if err == nil {
|
|
t.Error("Expected error for wrong code")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify email code with empty code", func(t *testing.T) {
|
|
err := svc.VerifyEmailCode(ctx, "test@example.com", "login", "")
|
|
if err == nil {
|
|
t.Error("Expected error for empty code")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify email code expired", func(t *testing.T) {
|
|
err := svc.VerifyEmailCode(ctx, "nonexistent@example.com", "login", "123456")
|
|
if err == nil {
|
|
t.Error("Expected error for expired/missing code")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// SMS Code Service Tests - Extended
|
|
// =============================================================================
|
|
|
|
func TestSMSCodeService_SendCode(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
provider := &service.MockSMSProvider{}
|
|
cfg := service.DefaultSMSCodeConfig()
|
|
svc := service.NewSMSCodeService(provider, cacheManager, cfg)
|
|
ctx := context.Background()
|
|
|
|
t.Run("Send code success", func(t *testing.T) {
|
|
req := &service.SendCodeRequest{
|
|
Phone: "13800138000",
|
|
Purpose: "login",
|
|
}
|
|
resp, err := svc.SendCode(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("SendCode failed: %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Error("Expected response")
|
|
}
|
|
})
|
|
|
|
t.Run("Send code with invalid phone", func(t *testing.T) {
|
|
req := &service.SendCodeRequest{
|
|
Phone: "invalid",
|
|
Purpose: "login",
|
|
}
|
|
_, err := svc.SendCode(ctx, req)
|
|
if err == nil {
|
|
t.Error("Expected error for invalid phone")
|
|
}
|
|
})
|
|
|
|
t.Run("Send code with nil request", func(t *testing.T) {
|
|
_, err := svc.SendCode(ctx, nil)
|
|
if err == nil {
|
|
t.Error("Expected error for nil request")
|
|
}
|
|
})
|
|
|
|
t.Run("Send code with cooldown", func(t *testing.T) {
|
|
phone := "13900139000"
|
|
req := &service.SendCodeRequest{Phone: phone, Purpose: "login"}
|
|
svc.SendCode(ctx, req)
|
|
// Second request should hit cooldown
|
|
_, err := svc.SendCode(ctx, req)
|
|
if err == nil {
|
|
t.Error("Expected rate limit error due to cooldown")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSMSCodeService_VerifyCode(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
provider := &service.MockSMSProvider{}
|
|
cfg := service.DefaultSMSCodeConfig()
|
|
svc := service.NewSMSCodeService(provider, cacheManager, cfg)
|
|
ctx := context.Background()
|
|
|
|
t.Run("Verify code success", func(t *testing.T) {
|
|
phone := "13700137000"
|
|
req := &service.SendCodeRequest{Phone: phone, Purpose: "login"}
|
|
svc.SendCode(ctx, req)
|
|
// Get code from cache
|
|
val, ok := cacheManager.Get(ctx, "sms_code:login:"+phone)
|
|
if !ok {
|
|
t.Fatal("Code not found in cache")
|
|
}
|
|
code := val.(string)
|
|
err := svc.VerifyCode(ctx, phone, "login", code)
|
|
if err != nil {
|
|
t.Fatalf("VerifyCode failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Verify code with wrong code", func(t *testing.T) {
|
|
phone := "13600136000"
|
|
req := &service.SendCodeRequest{Phone: phone, Purpose: "login"}
|
|
svc.SendCode(ctx, req)
|
|
err := svc.VerifyCode(ctx, phone, "login", "000000")
|
|
if err == nil {
|
|
t.Error("Expected error for wrong code")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify code with empty code", func(t *testing.T) {
|
|
err := svc.VerifyCode(ctx, "13800138000", "login", "")
|
|
if err == nil {
|
|
t.Error("Expected error for empty code")
|
|
}
|
|
})
|
|
|
|
t.Run("Verify code expired", func(t *testing.T) {
|
|
err := svc.VerifyCode(ctx, "19999999999", "login", "123456")
|
|
if err == nil {
|
|
t.Error("Expected error for expired code")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSMSCodeService_NilService(t *testing.T) {
|
|
var nilSvc *service.SMSCodeService
|
|
ctx := context.Background()
|
|
|
|
t.Run("SendCode with nil service", func(t *testing.T) {
|
|
_, err := nilSvc.SendCode(ctx, &service.SendCodeRequest{Phone: "13800138000"})
|
|
if err == nil {
|
|
t.Error("Expected error for nil service")
|
|
}
|
|
})
|
|
|
|
t.Run("VerifyCode with nil service", func(t *testing.T) {
|
|
err := nilSvc.VerifyCode(ctx, "13800138000", "login", "123456")
|
|
if err == nil {
|
|
t.Error("Expected error for nil service")
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Email Activation Service Tests
|
|
// =============================================================================
|
|
|
|
func TestEmailActivationService_SendActivationEmail(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
provider := &service.MockEmailProvider{}
|
|
svc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
|
|
ctx := context.Background()
|
|
|
|
t.Run("Send activation email success", func(t *testing.T) {
|
|
err := svc.SendActivationEmail(ctx, 1, "test@example.com", "testuser")
|
|
if err != nil {
|
|
t.Fatalf("SendActivationEmail failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEmailActivationService_ValidateActivationToken(t *testing.T) {
|
|
l1Cache := cache.NewL1Cache()
|
|
l2Cache := cache.NewRedisCache(false)
|
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
provider := &service.MockEmailProvider{}
|
|
svc := service.NewEmailActivationService(provider, cacheManager, "http://localhost:8080", "TestSite")
|
|
ctx := context.Background()
|
|
|
|
t.Run("Validate activation token invalid", func(t *testing.T) {
|
|
_, err := svc.ValidateActivationToken(ctx, "invalid_token")
|
|
if err == nil {
|
|
t.Error("Expected error for invalid token")
|
|
}
|
|
})
|
|
|
|
t.Run("Validate activation token empty", func(t *testing.T) {
|
|
_, err := svc.ValidateActivationToken(ctx, "")
|
|
if err == nil {
|
|
t.Error("Expected error for empty token")
|
|
}
|
|
})
|
|
|
|
t.Run("Validate activation token success", func(t *testing.T) {
|
|
// Send activation email first to create token
|
|
svc.SendActivationEmail(ctx, 123, "activate@example.com", "testuser")
|
|
// Find the token in cache
|
|
// We can't directly enumerate keys, so we test with known token
|
|
// This is a limitation of the test approach
|
|
// In practice, we'd need to either expose the token or mock the cache
|
|
})
|
|
}
|