veza/veza-backend-api/internal/services/account_lockout_service_test.go
senke 286be8ba1d
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Storybook Audit / Build & audit Storybook (push) Failing after 0s
chore(v0.102): consolidate remaining changes — docs, frontend, backend
- docs: SCOPE_CONTROL, CONTRIBUTING, README, .github templates
- frontend: DeveloperDashboardView, Player components, MSW handlers, auth, reactQuerySync
- backend: playback_analytics, playlist_service, testutils, integration README

Excluded (artifacts): .auth, playwright-report, test-results, storybook_audit_detailed.json
2026-02-20 13:02:12 +01:00

484 lines
12 KiB
Go

package services
import (
"context"
"os"
"testing"
"time"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"go.uber.org/zap"
)
func setupTestAccountLockoutService(t *testing.T) (*AccountLockoutService, *redis.Client, func()) {
redisURL := os.Getenv("REDIS_TEST_URL")
if redisURL == "" {
if testing.Short() {
t.Skip("Skipping AccountLockout test in short mode (requires Redis via testcontainers)")
return nil, nil, func() {}
}
// Use testcontainers if Redis URL not provided
ctx := context.Background()
redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "redis:7-alpine",
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
},
Started: true,
})
if err != nil {
t.Skipf("Skipping test: failed to start Redis container: %v", err)
return nil, nil, func() {}
}
endpoint, err := redisC.Endpoint(ctx, "")
if err != nil {
t.Skipf("Skipping test: failed to get Redis endpoint: %v", err)
return nil, nil, func() {}
}
client := redis.NewClient(&redis.Options{Addr: endpoint})
logger := zap.NewNop()
service := NewAccountLockoutService(client, logger)
cleanup := func() {
client.FlushDB(ctx)
client.Close()
redisC.Terminate(ctx)
}
return service, client, cleanup
}
// Use provided Redis URL
opts, err := redis.ParseURL(redisURL)
if err != nil {
t.Skipf("Skipping test: failed to parse Redis URL: %v", err)
return nil, nil, func() {}
}
client := redis.NewClient(opts)
ctx := context.Background()
_, err = client.Ping(ctx).Result()
if err != nil {
t.Skipf("Skipping test: Redis not available: %v", err)
return nil, nil, func() {}
}
client.FlushDB(ctx)
logger := zap.NewNop()
service := NewAccountLockoutService(client, logger)
cleanup := func() {
client.FlushDB(ctx)
client.Close()
}
return service, client, cleanup
}
func TestAccountLockoutService_NewAccountLockoutService(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
assert.NotNil(t, service)
assert.NotNil(t, service.redisClient)
assert.NotNil(t, service.logger)
assert.Equal(t, 5, service.maxAttempts)
assert.Equal(t, 30*time.Minute, service.lockoutDuration)
assert.Equal(t, 15*time.Minute, service.windowDuration)
}
func TestAccountLockoutService_NewAccountLockoutServiceWithConfig(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
config := &AccountLockoutConfig{
MaxAttempts: 10,
LockoutDuration: 60 * time.Minute,
WindowDuration: 30 * time.Minute,
}
newService := NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, config)
assert.NotNil(t, newService)
assert.Equal(t, 10, newService.maxAttempts)
assert.Equal(t, 60*time.Minute, newService.lockoutDuration)
assert.Equal(t, 30*time.Minute, newService.windowDuration)
}
func TestAccountLockoutService_NewAccountLockoutServiceWithConfig_NilConfig(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
newService := NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, nil)
assert.NotNil(t, newService)
assert.Equal(t, 5, newService.maxAttempts) // Default values
}
func TestAccountLockoutService_DefaultAccountLockoutConfig(t *testing.T) {
config := DefaultAccountLockoutConfig()
assert.NotNil(t, config)
assert.Equal(t, 5, config.MaxAttempts)
assert.Equal(t, 30*time.Minute, config.LockoutDuration)
assert.Equal(t, 15*time.Minute, config.WindowDuration)
}
func TestAccountLockoutService_RecordFailedAttempt_Success(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
// Verify attempt was recorded
count, err := service.GetFailedAttemptsCount(ctx, email)
assert.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestAccountLockoutService_RecordFailedAttempt_MultipleAttempts(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
// Record multiple failed attempts
for i := 0; i < 3; i++ {
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
}
// Verify count
count, err := service.GetFailedAttemptsCount(ctx, email)
assert.NoError(t, err)
assert.Equal(t, 3, count)
}
func TestAccountLockoutService_RecordFailedAttempt_LocksAfterMaxAttempts(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
// Create service with lower max attempts for testing
config := &AccountLockoutConfig{
MaxAttempts: 3,
LockoutDuration: 5 * time.Minute,
WindowDuration: 15 * time.Minute,
}
service = NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, config)
ctx := context.Background()
email := "test@example.com"
// Record attempts up to max
for i := 0; i < 3; i++ {
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
}
// Verify account is locked
locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.True(t, locked)
assert.NotNil(t, lockedUntil)
assert.True(t, lockedUntil.After(time.Now()))
}
func TestAccountLockoutService_RecordSuccessfulLogin_ResetsAttempts(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
// Record failed attempts
for i := 0; i < 3; i++ {
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
}
// Record successful login
err := service.RecordSuccessfulLogin(ctx, email)
assert.NoError(t, err)
// Verify attempts were reset
count, err := service.GetFailedAttemptsCount(ctx, email)
assert.NoError(t, err)
assert.Equal(t, 0, count)
}
func TestAccountLockoutService_RecordSuccessfulLogin_UnlocksAccount(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
// Create service with lower max attempts
config := &AccountLockoutConfig{
MaxAttempts: 2,
LockoutDuration: 5 * time.Minute,
WindowDuration: 15 * time.Minute,
}
service = NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, config)
ctx := context.Background()
email := "test@example.com"
// Lock account
for i := 0; i < 2; i++ {
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
}
// Verify locked
locked, _, err := service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.True(t, locked)
// Record successful login
err = service.RecordSuccessfulLogin(ctx, email)
assert.NoError(t, err)
// Verify unlocked
locked, _, err = service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.False(t, locked)
}
func TestAccountLockoutService_IsAccountLocked_NotLocked(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.False(t, locked)
assert.Nil(t, lockedUntil)
}
func TestAccountLockoutService_IsAccountLocked_Locked(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
// Lock account
err := service.LockAccount(ctx, email)
assert.NoError(t, err)
// Verify locked
locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.True(t, locked)
assert.NotNil(t, lockedUntil)
assert.True(t, lockedUntil.After(time.Now()))
}
func TestAccountLockoutService_LockAccount_Success(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
err := service.LockAccount(ctx, email)
assert.NoError(t, err)
// Verify locked
locked, _, err := service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.True(t, locked)
}
func TestAccountLockoutService_UnlockAccount_Success(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
// Lock account first
err := service.LockAccount(ctx, email)
assert.NoError(t, err)
// Unlock account
err = service.UnlockAccount(ctx, email)
assert.NoError(t, err)
// Verify unlocked
locked, _, err := service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.False(t, locked)
}
func TestAccountLockoutService_UnlockAccount_ResetsAttempts(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
// Record failed attempts
for i := 0; i < 3; i++ {
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
}
// Unlock account
err := service.UnlockAccount(ctx, email)
assert.NoError(t, err)
// Verify attempts were reset
count, err := service.GetFailedAttemptsCount(ctx, email)
assert.NoError(t, err)
assert.Equal(t, 0, count)
}
func TestAccountLockoutService_GetFailedAttemptsCount_Zero(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
count, err := service.GetFailedAttemptsCount(ctx, email)
assert.NoError(t, err)
assert.Equal(t, 0, count)
}
func TestAccountLockoutService_GetFailedAttemptsCount_WithAttempts(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
ctx := context.Background()
email := "test@example.com"
// Record failed attempts
for i := 0; i < 5; i++ {
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
}
count, err := service.GetFailedAttemptsCount(ctx, email)
assert.NoError(t, err)
assert.Equal(t, 5, count)
}
func TestAccountLockoutService_getAttemptsKey(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
email := "test@example.com"
key := service.getAttemptsKey(email)
assert.Equal(t, "account_lockout:attempts:test@example.com", key)
}
func TestAccountLockoutService_getLockoutKey(t *testing.T) {
service, _, cleanup := setupTestAccountLockoutService(t)
if service == nil {
return
}
defer cleanup()
email := "test@example.com"
key := service.getLockoutKey(email)
assert.Equal(t, "account_lockout:locked:test@example.com", key)
}
func TestAccountLockoutService_RecordFailedAttempt_NoRedis(t *testing.T) {
logger := zap.NewNop()
service := NewAccountLockoutService(nil, logger)
ctx := context.Background()
email := "test@example.com"
// Should not error even without Redis
err := service.RecordFailedAttempt(ctx, email)
assert.NoError(t, err)
}
func TestAccountLockoutService_IsAccountLocked_NoRedis(t *testing.T) {
logger := zap.NewNop()
service := NewAccountLockoutService(nil, logger)
ctx := context.Background()
email := "test@example.com"
locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
assert.NoError(t, err)
assert.False(t, locked)
assert.Nil(t, lockedUntil)
}
func TestAccountLockoutService_AccountLockoutConfig_Fields(t *testing.T) {
config := &AccountLockoutConfig{
MaxAttempts: 10,
LockoutDuration: 60 * time.Minute,
WindowDuration: 30 * time.Minute,
}
assert.Equal(t, 10, config.MaxAttempts)
assert.Equal(t, 60*time.Minute, config.LockoutDuration)
assert.Equal(t, 30*time.Minute, config.WindowDuration)
}