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(ctx context.Context, 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(ctx context.Context, 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) ValidateTokenWithProvider(ctx context.Context, provider auth.OAuthProvider, 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") } }) }