Files
john-okeefe 0438ec4625 refactor(middleware): fix type signatures for Echo v5 compatibility
Update all middleware functions to use *echo.Context (pointer) instead of echo.Context (value) as required by Echo v5.

Changes in device_auth.go:
- Update DeviceAuthMiddleware() signature (line 38)
- Update validateDeviceAuth() signature (line 170)
- Update RequireDeviceAuth() signature (line 212)

Changes in error_handler.go:
- Update RespondWithError() signature (line 44)
- Update RespondWithHTTPError() signature (line 69)
- Update WrapHandler() to accept *echo.Context (line 82)
- Fix context passing in WrapHandler() (c is already pointer)

Changes in rate_limiter.go:
- Update RateLimiterMiddleware() signature (line 102)

Changes in request_tracing.go:
- Update RequestTracingMiddleware() signature (line 48)
- Fix Response() dereference for v5 API (line 264)
  - Use *c.Response() to get http.ResponseWriter

Changes in security.go:
- Update SecurityHeadersMiddleware() signature (line 14)

Changes in device_auth_test.go:
- Update test helper signatures

Changes in middleware_test.go:
- Remove unused import

All middleware now properly implements Echo v5's pointer-based context pattern.
2026-03-06 14:00:05 -05:00

319 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)
}
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)
}