test: add comprehensive test coverage for API endpoints and services
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
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, "scan", config)
|
||||
assert.Equal(t, 10, remaining)
|
||||
|
||||
// Use 3 requests
|
||||
for i := 0; i < 3; i++ {
|
||||
limiter.CheckRateLimit(deviceID, "scan", config)
|
||||
}
|
||||
|
||||
// Should have 7 remaining
|
||||
remaining = limiter.GetRemainingRequests(deviceID, "scan", 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", 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)
|
||||
}
|
||||
Reference in New Issue
Block a user