package service import ( "context" "errors" "fmt" "strings" "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" ) type failingL2Cache struct { setErr error } func (f *failingL2Cache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { return f.setErr } func (f *failingL2Cache) Get(ctx context.Context, key string) (interface{}, error) { return nil, nil } func (f *failingL2Cache) Delete(ctx context.Context, key string) error { return nil } func (f *failingL2Cache) Exists(ctx context.Context, key string) (bool, error) { return false, nil } func (f *failingL2Cache) Clear(ctx context.Context) error { return nil } func (f *failingL2Cache) Increment(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, error) { return 0, nil } func (f *failingL2Cache) Close() error { return nil } func TestAuthService_Logout_FailsClosedWhenBlacklistWriteFails(t *testing.T) { dsn := fmt.Sprintf("file:logoutfailclosed_%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("open db failed: %v", err) } if err := db.AutoMigrate(&domain.User{}, &domain.Role{}, &domain.UserRole{}, &domain.LoginLog{}, &domain.PasswordHistory{}); err != nil { t.Fatalf("migrate failed: %v", err) } for _, role := range domain.PredefinedRoles { roleCopy := role if err := db.Create(&roleCopy).Error; err != nil { t.Fatalf("seed role %s failed: %v", role.Code, err) } } jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{ HS256Secret: fmt.Sprintf("logout-failclosed-secret-%d", time.Now().UnixNano()), AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } userRepo := repository.NewUserRepository(db) userRoleRepo := repository.NewUserRoleRepository(db) roleRepo := repository.NewRoleRepository(db) cacheManager := cache.NewCacheManager(cache.NewL1Cache(), &failingL2Cache{setErr: errors.New("forced blacklist failure")}) authSvc := NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute) authSvc.SetRoleRepositories(userRoleRepo, roleRepo) ctx := context.Background() if _, err := authSvc.Register(ctx, &RegisterRequest{Username: "logoutfail", Password: "Password123!"}); err != nil { t.Fatalf("register failed: %v", err) } loginResp, err := authSvc.Login(ctx, &LoginRequest{Username: "logoutfail", Password: "Password123!"}, "127.0.0.1") if err != nil { t.Fatalf("login failed: %v", err) } err = authSvc.Logout(ctx, "logoutfail", &LogoutRequest{AccessToken: loginResp.AccessToken, RefreshToken: loginResp.RefreshToken}) if err == nil { t.Fatal("expected logout to fail closed when blacklist write fails") } if !strings.Contains(err.Error(), "forced blacklist failure") { t.Fatalf("expected propagated blacklist error, got: %v", err) } }