package service_test import ( "context" "encoding/json" "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 Social Account Binding Tests // ============================================================================= type socialTestEnv struct { db *gorm.DB authSvc *service.AuthService userRepo *repository.UserRepository socialRepo repository.SocialAccountRepository } func setupSocialTestEnv(t *testing.T) *socialTestEnv { t.Helper() dsn := fmt.Sprintf("file:social_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.SocialAccount{}); err != nil { t.Fatalf("failed to migrate: %v", err) } 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) socialRepo, err := repository.NewSocialAccountRepository(db) if err != nil { t.Fatalf("failed to create social account repository: %v", err) } l1Cache := cache.NewL1Cache() l2Cache := cache.NewRedisCache(false) cacheManager := cache.NewCacheManager(l1Cache, l2Cache) // Pass socialRepo to NewAuthService so GetSocialAccounts works authSvc := service.NewAuthService(userRepo, socialRepo, jwtManager, cacheManager, 8, 5, 15*time.Minute) return &socialTestEnv{ db: db, authSvc: authSvc, userRepo: userRepo, socialRepo: socialRepo, } } func TestAuthService_GetSocialAccounts(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create test user user := &domain.User{ Username: "socialuser", Password: "$2a$10$hash", Status: domain.UserStatusActive, } env.db.Create(user) t.Run("Get social accounts with nil service", func(t *testing.T) { var nilSvc *service.AuthService accounts, err := nilSvc.GetSocialAccounts(ctx, user.ID) if err != nil { t.Errorf("Expected nil error for nil service, got: %v", err) } if len(accounts) != 0 { t.Errorf("Expected empty accounts for nil service, got: %d", len(accounts)) } }) t.Run("Get social accounts for user with no accounts", func(t *testing.T) { accounts, err := env.authSvc.GetSocialAccounts(ctx, user.ID) if err != nil { t.Fatalf("GetSocialAccounts failed: %v", err) } if len(accounts) != 0 { t.Errorf("Expected empty accounts, got: %d", len(accounts)) } }) t.Run("Get social accounts for user with accounts", func(t *testing.T) { // Create social accounts socialAccount := &domain.SocialAccount{ UserID: user.ID, Provider: "github", OpenID: "github123", Status: domain.SocialAccountStatusActive, } env.db.Create(socialAccount) accounts, err := env.authSvc.GetSocialAccounts(ctx, user.ID) if err != nil { t.Fatalf("GetSocialAccounts failed: %v", err) } if len(accounts) != 1 { t.Errorf("Expected 1 account, got: %d", len(accounts)) } if accounts[0].Provider != "github" { t.Errorf("Expected provider 'github', got: %s", accounts[0].Provider) } }) } func TestAuthService_BindSocialAccount(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create test user user := &domain.User{ Username: "binduser", Password: "$2a$10$hash", Status: domain.UserStatusActive, } env.db.Create(user) t.Run("Bind social account with nil service", func(t *testing.T) { var nilSvc *service.AuthService err := nilSvc.BindSocialAccount(ctx, user.ID, "github", "openid123") if err == nil { t.Error("Expected error for nil service") } }) t.Run("Bind social account for non-existent user", func(t *testing.T) { err := env.authSvc.BindSocialAccount(ctx, 9999, "github", "openid123") if err == nil { t.Error("Expected error for non-existent user") } }) t.Run("Bind social account for inactive user", func(t *testing.T) { inactiveUser := &domain.User{ Username: "inactivesocial", Password: "$2a$10$hash", Status: domain.UserStatusInactive, } env.db.Create(inactiveUser) err := env.authSvc.BindSocialAccount(ctx, inactiveUser.ID, "github", "openid456") if err == nil { t.Error("Expected error for inactive user") } }) t.Run("Bind social account with empty provider", func(t *testing.T) { err := env.authSvc.BindSocialAccount(ctx, user.ID, "", "openid123") if err == nil { t.Error("Expected error for empty provider") } }) t.Run("Bind social account with empty openID", func(t *testing.T) { err := env.authSvc.BindSocialAccount(ctx, user.ID, "github", "") if err == nil { t.Error("Expected error for empty openID") } }) t.Run("Bind social account success", func(t *testing.T) { err := env.authSvc.BindSocialAccount(ctx, user.ID, "google", "google789") if err != nil { t.Fatalf("BindSocialAccount failed: %v", err) } // Verify binding accounts, _ := env.authSvc.GetSocialAccounts(ctx, user.ID) if len(accounts) == 0 { t.Error("Expected social account to be created") } }) t.Run("Bind same provider with same openID (idempotent)", func(t *testing.T) { err := env.authSvc.BindSocialAccount(ctx, user.ID, "google", "google789") if err != nil { t.Fatalf("Expected no error for same binding: %v", err) } }) t.Run("Bind same provider with different openID", func(t *testing.T) { err := env.authSvc.BindSocialAccount(ctx, user.ID, "google", "different_openid") if err == nil { t.Error("Expected error for different openID on same provider") } }) } func TestAuthService_BindSocialAccount_AlreadyBound(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create two users user1 := &domain.User{ Username: "binduser1", Password: "$2a$10$hash", Status: domain.UserStatusActive, } env.db.Create(user1) user2 := &domain.User{ Username: "binduser2", Password: "$2a$10$hash", Status: domain.UserStatusActive, } env.db.Create(user2) // Bind social account to user1 env.authSvc.BindSocialAccount(ctx, user1.ID, "wechat", "wechat123") // Try to bind same openID to user2 err := env.authSvc.BindSocialAccount(ctx, user2.ID, "wechat", "wechat123") if err == nil { t.Error("Expected error when binding already bound account") } } func TestAuthService_UnbindSocialAccount(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create test user with password hashedPassword, _ := auth.HashPassword("Password123!") user := &domain.User{ Username: "unbinduser", Password: hashedPassword, Status: domain.UserStatusActive, } env.db.Create(user) // Create social account socialAccount := &domain.SocialAccount{ UserID: user.ID, Provider: "github", OpenID: "github123", Status: domain.SocialAccountStatusActive, } env.db.Create(socialAccount) t.Run("Unbind social account with nil service", func(t *testing.T) { var nilSvc *service.AuthService err := nilSvc.UnbindSocialAccount(ctx, user.ID, "github", "Password123!", "") if err == nil { t.Error("Expected error for nil service") } }) t.Run("Unbind social account for non-existent user", func(t *testing.T) { err := env.authSvc.UnbindSocialAccount(ctx, 9999, "github", "Password123!", "") if err == nil { t.Error("Expected error for non-existent user") } }) t.Run("Unbind social account not bound", func(t *testing.T) { err := env.authSvc.UnbindSocialAccount(ctx, user.ID, "nonexistent_provider", "Password123!", "") if err == nil { t.Error("Expected error for non-bound provider") } }) t.Run("Unbind social account with wrong password", func(t *testing.T) { err := env.authSvc.UnbindSocialAccount(ctx, user.ID, "github", "wrongpassword", "") if err == nil { t.Error("Expected error for wrong password") } }) } // ============================================================================= // Verify Sensitive Action Tests // ============================================================================= func TestAuthService_VerifySensitiveAction(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() t.Run("Verify with nil user", func(t *testing.T) { var nilSvc *service.AuthService err := nilSvc.VerifyTOTP(ctx, 1, "code", "") if err == nil { t.Error("Expected error for nil service") } }) t.Run("Verify with user without password or TOTP", func(t *testing.T) { user := &domain.User{ Username: "nosecretuser", Status: domain.UserStatusActive, } env.db.Create(user) err := env.authSvc.VerifyTOTP(ctx, user.ID, "123456", "") if err == nil { t.Error("Expected error when no verification method available") } }) } // ============================================================================= // Start Social Account Binding Tests // ============================================================================= func TestAuthService_StartSocialAccountBinding(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create test user with password hashedPassword, _ := auth.HashPassword("Password123!") user := &domain.User{ Username: "startbinduser", Password: hashedPassword, Status: domain.UserStatusActive, } env.db.Create(user) t.Run("Start binding with nil service", func(t *testing.T) { var nilSvc *service.AuthService _, _, err := nilSvc.StartSocialAccountBinding(ctx, user.ID, "github", "http://localhost", "Password123!", "") if err == nil { t.Error("Expected error for nil service") } }) t.Run("Start binding for non-existent user", func(t *testing.T) { _, _, err := env.authSvc.StartSocialAccountBinding(ctx, 9999, "github", "http://localhost", "Password123!", "") if err == nil { t.Error("Expected error for non-existent user") } }) t.Run("Start binding for inactive user", func(t *testing.T) { inactiveUser := &domain.User{ Username: "inactivestartbind", Password: hashedPassword, Status: domain.UserStatusInactive, } env.db.Create(inactiveUser) _, _, err := env.authSvc.StartSocialAccountBinding(ctx, inactiveUser.ID, "github", "http://localhost", "Password123!", "") if err == nil { t.Error("Expected error for inactive user") } }) t.Run("Start binding with wrong password", func(t *testing.T) { _, _, err := env.authSvc.StartSocialAccountBinding(ctx, user.ID, "github", "http://localhost", "wrongpassword", "") if err == nil { t.Error("Expected error for wrong password") } }) } // ============================================================================= // OAuth Bind Callback Tests // ============================================================================= func TestAuthService_OAuthBindCallback(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create test user user := &domain.User{ Username: "oauthcallbackuser", Password: "$2a$10$hash", Status: domain.UserStatusActive, } env.db.Create(user) t.Run("OAuth bind callback with nil service", func(t *testing.T) { var nilSvc *service.AuthService _, err := nilSvc.OAuthBindCallback(ctx, user.ID, "github", "code123") if err == nil { t.Error("Expected error for nil service") } }) t.Run("OAuth bind callback for non-existent user", func(t *testing.T) { _, err := env.authSvc.OAuthBindCallback(ctx, 9999, "github", "code123") if err == nil { t.Error("Expected error for non-existent user") } }) t.Run("OAuth bind callback for inactive user", func(t *testing.T) { inactiveUser := &domain.User{ Username: "inactivecallback", Password: "$2a$10$hash", Status: domain.UserStatusInactive, } env.db.Create(inactiveUser) _, err := env.authSvc.OAuthBindCallback(ctx, inactiveUser.ID, "github", "code123") if err == nil { t.Error("Expected error for inactive user") } }) } // ============================================================================= // Verify TOTP Tests // ============================================================================= func TestAuthService_VerifyTOTP(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() t.Run("Verify TOTP with nil service", func(t *testing.T) { var nilSvc *service.AuthService err := nilSvc.VerifyTOTP(ctx, 1, "123456", "") if err == nil { t.Error("Expected error for nil service") } }) t.Run("Verify TOTP for non-existent user", func(t *testing.T) { err := env.authSvc.VerifyTOTP(ctx, 9999, "123456", "") if err == nil { t.Error("Expected error for non-existent user") } }) t.Run("Verify TOTP for user without TOTP", func(t *testing.T) { user := &domain.User{ Username: "nototpverify", Password: "$2a$10$hash", Status: domain.UserStatusActive, } env.db.Create(user) err := env.authSvc.VerifyTOTP(ctx, user.ID, "123456", "") if err == nil { t.Error("Expected error for user without TOTP") } }) } func TestAuthService_VerifyTOTPWithTrustedDevice(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create user with TOTP user := &domain.User{ Username: "totptrusted", Password: "$2a$10$hash", Status: domain.UserStatusActive, TOTPEnabled: true, TOTPSecret: "JBSWY3DPEHPK3PXP", // test secret } env.db.Create(user) // Create device service deviceRepo := repository.NewDeviceRepository(env.db) userRepo := repository.NewUserRepository(env.db) deviceSvc := service.NewDeviceService(deviceRepo, userRepo) // Update auth service with device service authSvcWithDevice := service.NewAuthService(userRepo, nil, nil, nil, 8, 5, 15*time.Minute) authSvcWithDevice.SetDeviceService(deviceSvc) t.Run("Verify TOTP without device ID", func(t *testing.T) { err := authSvcWithDevice.VerifyTOTP(ctx, user.ID, "123456", "") if err == nil { // Should fail because the code is wrong } }) t.Run("Verify TOTP with non-existent device", func(t *testing.T) { err := authSvcWithDevice.VerifyTOTP(ctx, user.ID, "123456", "nonexistent_device") if err == nil { // Should fail because device doesn't exist } }) } // ============================================================================= // Verify TOTP Code or Recovery Code Tests // ============================================================================= func TestAuthService_VerifyTOTPCodeOrRecoveryCode(t *testing.T) { // Create recovery codes hash recoveryCodes := []string{"code1", "code2", "code3"} recoveryCodesJSON, _ := json.Marshal(recoveryCodes) user := &domain.User{ Username: "recoveryuser", Password: "$2a$10$hash", Status: domain.UserStatusActive, TOTPEnabled: true, TOTPSecret: "JBSWY3DPEHPK3PXP", TOTPRecoveryCodes: string(recoveryCodesJSON), } t.Run("User has TOTP enabled but wrong code", func(t *testing.T) { // This tests the logic path where TOTP validation fails // The function should try recovery codes if !user.TOTPEnabled { t.Error("Expected TOTP to be enabled") } }) } // ============================================================================= // Login By Code Tests // ============================================================================= func TestAuthService_LoginByCode(t *testing.T) { env := setupSocialTestEnv(t) ctx := context.Background() // Create test user with phone phone := "13800138000" user := &domain.User{ Username: "logincodeuser", Phone: &phone, Password: "$2a$10$hash", Status: domain.UserStatusActive, } env.db.Create(user) t.Run("Login by code with nil service", func(t *testing.T) { var nilSvc *service.AuthService _, err := nilSvc.LoginByCode(ctx, "13800138000", "123456", "127.0.0.1") if err == nil { t.Error("Expected error for nil service") } }) t.Run("Login by code with empty phone", func(t *testing.T) { _, err := env.authSvc.LoginByCode(ctx, "", "123456", "127.0.0.1") if err == nil { t.Error("Expected error for empty phone") } }) t.Run("Login by code without SMS service configured", func(t *testing.T) { _, err := env.authSvc.LoginByCode(ctx, "13800138000", "123456", "127.0.0.1") if err == nil { t.Error("Expected error when SMS service not configured") } }) }