package middleware import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestPasswordValidator_ValidatePassword(t *testing.T) { tests := []struct { name string password string wantValid bool }{ { name: "valid password with all requirements", password: "Test@Pass123!", wantValid: true, }, { name: "valid password with special chars", password: "MyP@ssw0rd#2024", wantValid: true, }, { name: "too short", password: "Test1!", wantValid: false, }, { name: "no uppercase", password: "test@pass123!", wantValid: false, }, { name: "no lowercase", password: "TEST@PASS123!", wantValid: false, }, { name: "no number", password: "Test@Password!", wantValid: false, }, { name: "no special character", password: "TestPassword123", wantValid: false, }, { name: "empty string", password: "", wantValid: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePassword(tt.password) if tt.wantValid { assert.NoError(t, err) } else { assert.Error(t, err) } }) } } func TestPasswordValidator_ErrorMessages(t *testing.T) { tests := []struct { name string password string expectedInError string }{ { name: "too short error", password: "Short1!", expectedInError: "at least 8 characters", }, { name: "no uppercase error", password: "alllower123!", expectedInError: "uppercase letter", }, { name: "no lowercase error", password: "ALLUPPER123!", expectedInError: "lowercase letter", }, { name: "no number error", password: "NoNumbers!", expectedInError: "number", }, { name: "no special char error", password: "NoSpecialChars123", expectedInError: "special character", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePassword(tt.password) assert.Error(t, err) assert.Contains(t, err.Error(), tt.expectedInError) }) } } func TestGetPasswordRequirements(t *testing.T) { requirements := GetPasswordRequirements() assert.NotEmpty(t, requirements) assert.Greater(t, len(requirements), 3) // Check for common requirements requirementText := "" for _, req := range requirements { requirementText += req + " " } assert.Contains(t, requirementText, "8") assert.Contains(t, requirementText, "uppercase") assert.Contains(t, requirementText, "lowercase") assert.Contains(t, requirementText, "number") assert.Contains(t, requirementText, "special") } func TestLoginAttemptTracker_RecordFailedAttempt(t *testing.T) { tracker := NewLoginAttemptTracker(3, 5*time.Minute, 5*time.Minute) username := "testuser" // First failed attempt locked, remainingTime := tracker.RecordFailedAttempt(username) assert.False(t, locked) assert.Equal(t, time.Duration(0), remainingTime) // Second failed attempt locked, remainingTime = tracker.RecordFailedAttempt(username) assert.False(t, locked) assert.Equal(t, time.Duration(0), remainingTime) // Third failed attempt - should lock locked, remainingTime = tracker.RecordFailedAttempt(username) assert.True(t, locked) assert.Greater(t, remainingTime, time.Duration(0)) // Verify user is locked locked, _ = tracker.IsLocked(username) assert.True(t, locked) // Clear attempts tracker.ClearAttempts(username) // Should no longer be locked locked, _ = tracker.IsLocked(username) assert.False(t, locked) } func TestLoginAttemptTracker_IsLocked(t *testing.T) { tracker := NewLoginAttemptTracker(3, 5*time.Minute, 5*time.Minute) username := "lockeduser" // Record failed attempts up to max for i := 0; i < 3; i++ { tracker.RecordFailedAttempt(username) } // Verify user is locked locked, remainingTime := tracker.IsLocked(username) assert.True(t, locked) assert.Greater(t, remainingTime, time.Duration(0)) // Clear attempts tracker.ClearAttempts(username) // Should no longer be locked locked, remainingTime = tracker.IsLocked(username) assert.False(t, locked) assert.Equal(t, time.Duration(0), remainingTime) } func TestLoginAttemptTracker_ConcurrentAccess(t *testing.T) { tracker := NewLoginAttemptTracker(5, 5*time.Minute, 5*time.Minute) done := make(chan bool, 10) // Concurrent access from multiple goroutines for i := 0; i < 10; i++ { go func(index int) { username := "user" + string(rune('0'+index)) tracker.RecordFailedAttempt(username) tracker.IsLocked(username) tracker.ClearAttempts(username) done <- true }(i) } // Wait for all goroutines for i := 0; i < 10; i++ { <-done } // Should complete without deadlock or race } func TestDeviceRateLimiter_CheckRateLimit(t *testing.T) { limiter := NewDeviceRateLimiter() config := DeviceRateLimitConfig{ SyncRequestsPerMinute: 5, } deviceID := "test-device-123" requestType := "sync" // First 5 requests should succeed for i := 0; i < 5; i++ { allowed := limiter.CheckRateLimit(deviceID, requestType, config) assert.True(t, allowed, "Request %d should be allowed", i+1) } // 6th request should be rate limited allowed := limiter.CheckRateLimit(deviceID, requestType, config) assert.False(t, allowed, "Request 6 should be rate limited") // Get remaining requests remaining := limiter.GetRemainingRequests(deviceID, requestType, config) assert.Equal(t, 0, remaining) // Reset and verify limiter.Reset(deviceID) // Should be allowed again allowed = limiter.CheckRateLimit(deviceID, requestType, config) assert.True(t, allowed, "Request after reset should be allowed") } func TestDeviceRateLimiter_DifferentDevices(t *testing.T) { limiter := NewDeviceRateLimiter() config := DeviceRateLimitConfig{ SyncRequestsPerMinute: 2, } // Exhaust limit for device1 for i := 0; i < 2; i++ { limiter.CheckRateLimit("device1", "sync", config) } // Device1 should be rate limited allowed := limiter.CheckRateLimit("device1", "sync", config) assert.False(t, allowed) // Device2 should still work allowed = limiter.CheckRateLimit("device2", "sync", config) assert.True(t, allowed) } func TestDeviceRateLimiter_GetRemainingRequests(t *testing.T) { limiter := NewDeviceRateLimiter() config := DeviceRateLimitConfig{ SyncRequestsPerMinute: 10, } deviceID := "test-device-456" // Initially should have all requests remaining remaining := limiter.GetRemainingRequests(deviceID, "sync", config) assert.Equal(t, 10, remaining) // Use 3 requests for i := 0; i < 3; i++ { limiter.CheckRateLimit(deviceID, "sync", config) } // Should have 7 remaining remaining = limiter.GetRemainingRequests(deviceID, "sync", config) assert.Equal(t, 7, remaining) } func TestNewRateLimiter(t *testing.T) { config := DefaultRateLimiterConfig() limiter := NewRateLimiter(config) assert.NotNil(t, limiter) } func TestHTTPError_Error(t *testing.T) { err := NewHTTPError(404, "Not Found", nil) assert.NotNil(t, err) assert.Equal(t, 404, err.Code) assert.Equal(t, "Not Found", err.Message) } func TestHTTPError_ErrorWithInternal(t *testing.T) { internalErr := assert.AnError err := NewHTTPError(500, "Internal Error", internalErr) assert.Equal(t, "Internal Error: assert.AnError general error for testing", err.Error()) assert.Equal(t, 500, err.Code) assert.Equal(t, "Internal Error", err.Message) assert.Equal(t, internalErr, err.Err) } func TestNewHTTPError(t *testing.T) { err := NewHTTPError(404, "Not Found", nil) assert.NotNil(t, err) assert.Equal(t, 404, err.Code) assert.Equal(t, "Not Found", err.Message) }