Files
user-system/internal/service/auth_oauth_internal_test.go
long-agent 582ad7a069 test: add comprehensive test coverage and improve code quality
- 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)
2026-04-17 20:43:50 +08:00

450 lines
14 KiB
Go

package service
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"
gormsqlite "gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// =============================================================================
// Mock OAuth Manager
// =============================================================================
type mockOAuthManager struct {
authURL string
exchangeErr error
userInfoErr error
oauthUser *auth.OAuthUser
providers []auth.OAuthProviderInfo
config *auth.OAuthConfig
}
func (m *mockOAuthManager) GetAuthURL(provider auth.OAuthProvider, state string) (string, error) {
return m.authURL, nil
}
func (m *mockOAuthManager) ExchangeCode(provider auth.OAuthProvider, code string) (*auth.OAuthToken, error) {
if m.exchangeErr != nil {
return nil, m.exchangeErr
}
return &auth.OAuthToken{AccessToken: "mock-token"}, nil
}
func (m *mockOAuthManager) GetUserInfo(provider auth.OAuthProvider, token *auth.OAuthToken) (*auth.OAuthUser, error) {
if m.userInfoErr != nil {
return nil, m.userInfoErr
}
if m.oauthUser != nil {
return m.oauthUser, nil
}
return &auth.OAuthUser{
OpenID: "mock-openid",
UnionID: "mock-unionid",
Nickname: "Mock User",
Email: "mock@test.com",
Avatar: "https://example.com/avatar.png",
}, nil
}
func (m *mockOAuthManager) ValidateToken(token string) (bool, error) {
return token != "", nil
}
func (m *mockOAuthManager) GetConfig(provider auth.OAuthProvider) (*auth.OAuthConfig, bool) {
if m.config != nil {
return m.config, true
}
return nil, false
}
func (m *mockOAuthManager) GetEnabledProviders() []auth.OAuthProviderInfo {
return m.providers
}
// =============================================================================
// LoginByCode Internal Tests
// =============================================================================
func setupLoginByCodeInternalTestEnv(t *testing.T) (*AuthService, *gorm.DB) {
t.Helper()
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: fmt.Sprintf("file:logincode_internal_test_%d?mode=memory&cache=shared", time.Now().UnixNano()),
}), &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.LoginLog{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
userRepo := repository.NewUserRepository(db)
loginLogRepo := repository.NewLoginLogRepository(db)
socialRepo, _ := repository.NewSocialAccountRepository(db)
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: fmt.Sprintf("test-secret-%d", time.Now().UnixNano()),
AccessTokenExpire: 15 * time.Minute,
RefreshTokenExpire: 7 * 24 * time.Hour,
})
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
svc := NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute)
svc.SetLoginLogRepository(loginLogRepo)
return svc, db
}
func TestLoginByCode_Internal(t *testing.T) {
ctx := context.Background()
t.Run("LoginByCode with nil service", func(t *testing.T) {
var nilSvc *AuthService
_, err := nilSvc.LoginByCode(ctx, "13800138000", "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error for nil service")
}
})
t.Run("LoginByCode without SMS service configured", func(t *testing.T) {
svc, _ := setupLoginByCodeInternalTestEnv(t)
_, err := svc.LoginByCode(ctx, "13800138000", "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error when SMS service not configured")
}
})
t.Run("LoginByCode with empty phone", func(t *testing.T) {
svc, _ := setupLoginByCodeInternalTestEnv(t)
smsProvider := &mockSMSProvider{}
smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
svc.SetSMSCodeService(smsCodeSvc)
_, err := svc.LoginByCode(ctx, "", "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error for empty phone")
}
})
t.Run("LoginByCode for non-existent phone", func(t *testing.T) {
svc, _ := setupLoginByCodeInternalTestEnv(t)
smsProvider := &mockSMSProvider{}
smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
svc.SetSMSCodeService(smsCodeSvc)
_, err := svc.LoginByCode(ctx, "19999999999", "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error for non-existent phone")
}
})
t.Run("LoginByCode for locked user", func(t *testing.T) {
svc, db := setupLoginByCodeInternalTestEnv(t)
smsProvider := &mockSMSProvider{}
smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
svc.SetSMSCodeService(smsCodeSvc)
phone := "13800138002"
user := &domain.User{
Username: "lockeduser",
Phone: &phone,
Password: "$2a$10$hash",
Status: domain.UserStatusLocked,
}
db.Create(user)
_, err := svc.LoginByCode(ctx, "13800138002", "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error for locked user")
}
})
t.Run("LoginByCode for inactive user", func(t *testing.T) {
svc, db := setupLoginByCodeInternalTestEnv(t)
smsProvider := &mockSMSProvider{}
smsCodeSvc := NewSMSCodeService(smsProvider, &mockCacheForSMS{}, DefaultSMSCodeConfig())
svc.SetSMSCodeService(smsCodeSvc)
phone := "13800138003"
user := &domain.User{
Username: "inactiveuser",
Phone: &phone,
Password: "$2a$10$hash",
Status: domain.UserStatusInactive,
}
db.Create(user)
_, err := svc.LoginByCode(ctx, "13800138003", "123456", "127.0.0.1")
if err == nil {
t.Error("Expected error for inactive user")
}
})
t.Run("LoginByCode success", func(t *testing.T) {
svc, db := setupLoginByCodeInternalTestEnv(t)
cacheWithCode := &mockCacheWithGet{getResult: "123456", getFound: true}
smsCodeSvc := NewSMSCodeService(&mockSMSProvider{}, cacheWithCode, DefaultSMSCodeConfig())
svc.SetSMSCodeService(smsCodeSvc)
phone := "13800138004"
user := &domain.User{
Username: "successuser",
Phone: &phone,
Password: "$2a$10$hash",
Status: domain.UserStatusActive,
}
db.Create(user)
resp, err := svc.LoginByCode(ctx, "13800138004", "123456", "127.0.0.1")
if err != nil {
t.Fatalf("LoginByCode failed: %v", err)
}
if resp.AccessToken == "" {
t.Error("Expected access token")
}
})
}
// =============================================================================
// OAuthCallback Internal Tests
// =============================================================================
func TestOAuthCallback_Internal(t *testing.T) {
t.Run("OAuthCallback with nil service", func(t *testing.T) {
var nilSvc *AuthService
_, err := nilSvc.OAuthCallback(context.Background(), "github", "code123")
if err == nil {
t.Error("Expected error for nil service")
}
})
t.Run("OAuthCallback without OAuth manager", func(t *testing.T) {
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: fmt.Sprintf("file:oauth_no_manager_%d?mode=memory&cache=shared", time.Now().UnixNano()),
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("failed to connect database: %v", err)
}
db.AutoMigrate(&domain.User{}, &domain.SocialAccount{})
userRepo := repository.NewUserRepository(db)
socialRepo, _ := repository.NewSocialAccountRepository(db)
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: "test-secret",
AccessTokenExpire: 15 * time.Minute,
RefreshTokenExpire: 7 * 24 * time.Hour,
})
svc := NewAuthService(userRepo, socialRepo, jwtManager, nil, 8, 5, 15*time.Minute)
_, err = svc.OAuthCallback(context.Background(), "github", "code123")
if err == nil {
t.Error("Expected error when OAuth manager not configured")
}
})
t.Run("OAuthCallback with exchange error", func(t *testing.T) {
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: fmt.Sprintf("file:oauth_exchange_err_%d?mode=memory&cache=shared", time.Now().UnixNano()),
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("failed to connect database: %v", err)
}
db.AutoMigrate(&domain.User{}, &domain.SocialAccount{})
userRepo := repository.NewUserRepository(db)
socialRepo, _ := repository.NewSocialAccountRepository(db)
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: "test-secret",
AccessTokenExpire: 15 * time.Minute,
RefreshTokenExpire: 7 * 24 * time.Hour,
})
svc := NewAuthService(userRepo, socialRepo, jwtManager, nil, 8, 5, 15*time.Minute)
svc.oauthManager = &mockOAuthManager{exchangeErr: fmt.Errorf("exchange failed")}
_, err = svc.OAuthCallback(context.Background(), "github", "code123")
if err == nil {
t.Error("Expected error when exchange fails")
}
})
t.Run("OAuthCallback with user info error", func(t *testing.T) {
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: fmt.Sprintf("file:oauth_userinfo_err_%d?mode=memory&cache=shared", time.Now().UnixNano()),
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("failed to connect database: %v", err)
}
db.AutoMigrate(&domain.User{}, &domain.SocialAccount{})
userRepo := repository.NewUserRepository(db)
socialRepo, _ := repository.NewSocialAccountRepository(db)
jwtManager, _ := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: "test-secret",
AccessTokenExpire: 15 * time.Minute,
RefreshTokenExpire: 7 * 24 * time.Hour,
})
svc := NewAuthService(userRepo, socialRepo, jwtManager, nil, 8, 5, 15*time.Minute)
svc.oauthManager = &mockOAuthManager{userInfoErr: fmt.Errorf("user info failed")}
_, err = svc.OAuthCallback(context.Background(), "github", "code123")
if err == nil {
t.Error("Expected error when user info fails")
}
})
t.Run("OAuthCallback success with new user", func(t *testing.T) {
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: fmt.Sprintf("file:oauth_new_user_%d?mode=memory&cache=shared", time.Now().UnixNano()),
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("failed to connect database: %v", err)
}
db.AutoMigrate(&domain.User{}, &domain.SocialAccount{}, &domain.LoginLog{})
userRepo := repository.NewUserRepository(db)
socialRepo, _ := repository.NewSocialAccountRepository(db)
loginLogRepo := repository.NewLoginLogRepository(db)
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)
svc := NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute)
svc.oauthManager = &mockOAuthManager{}
svc.SetLoginLogRepository(loginLogRepo)
resp, err := svc.OAuthCallback(context.Background(), "github", "code123")
if err != nil {
t.Fatalf("OAuthCallback failed: %v", err)
}
if resp.AccessToken == "" {
t.Error("Expected access token")
}
})
t.Run("OAuthCallback success with existing social account", func(t *testing.T) {
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: fmt.Sprintf("file:oauth_existing_%d?mode=memory&cache=shared", time.Now().UnixNano()),
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("failed to connect database: %v", err)
}
db.AutoMigrate(&domain.User{}, &domain.SocialAccount{}, &domain.LoginLog{})
// Create existing user and social account
user := &domain.User{
Username: "existinguser",
Password: "$2a$10$hash",
Status: domain.UserStatusActive,
}
db.Create(user)
socialAccount := &domain.SocialAccount{
UserID: user.ID,
Provider: "github",
OpenID: "mock-openid",
Status: domain.SocialAccountStatusActive,
}
db.Create(socialAccount)
userRepo := repository.NewUserRepository(db)
socialRepo, _ := repository.NewSocialAccountRepository(db)
loginLogRepo := repository.NewLoginLogRepository(db)
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)
svc := NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute)
svc.oauthManager = &mockOAuthManager{}
svc.SetLoginLogRepository(loginLogRepo)
resp, err := svc.OAuthCallback(context.Background(), "github", "code123")
if err != nil {
t.Fatalf("OAuthCallback failed: %v", err)
}
if resp.AccessToken == "" {
t.Error("Expected access token")
}
if resp.User.Username != "existinguser" {
t.Errorf("Expected username 'existinguser', got %s", resp.User.Username)
}
})
}
// =============================================================================
// OAuthBindCallback Tests
// =============================================================================
func TestOAuthBindCallback_Internal(t *testing.T) {
t.Run("OAuthBindCallback with nil service", func(t *testing.T) {
var nilSvc *AuthService
_, err := nilSvc.OAuthBindCallback(context.Background(), 1, "github", "code123")
if err == nil {
t.Error("Expected error for nil service")
}
})
}
// =============================================================================
// StartSocialAccountBinding Tests
// =============================================================================
func TestStartSocialAccountBinding_Internal(t *testing.T) {
t.Run("StartSocialAccountBinding with nil service", func(t *testing.T) {
var nilSvc *AuthService
_, _, err := nilSvc.StartSocialAccountBinding(context.Background(), 1, "github", "", "", "")
if err == nil {
t.Error("Expected error for nil service")
}
})
}