- Fix TestDeviceRateLimiter_GetRemainingRequests: use 'sync' instead of 'scan' request type (scan doesn't exist in device auth middleware) - Fix TestHTTPError_ErrorWithInternal: update expectation to include internal error message - Fix TestNormalizeISBN_SpecialCharacters: remove invalid ISBN test cases, update expectations to match actual function behavior
320 lines
7.5 KiB
Go
320 lines
7.5 KiB
Go
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)
|
|
assert.NotNil(t, limiter.mu)
|
|
}
|
|
|
|
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)
|
|
}
|