- 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)
569 lines
16 KiB
Go
569 lines
16 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|
|
|