- 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
484 lines
12 KiB
Go
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)
|
|
}
|