Deadcode-verified removals (RTA reachability from main, cross-checked with -test and textual xrefs): Go server: - whole files: middleware/transaction.go, middleware/error_handler.go, middleware/password_validator.go (live registration machinery kept), handlers/sync.go, sync/offline.go - unrouted endpoints: auth UpdateUsername/UpdateEmail, timezone settings handler, OPDS RegisterOPDS, Kobo LibrarySync handler, ReaderHandler.ShowReader, JSON-data twins on collections/library - duplicate book-matching handlers (book_matching.go Handler twins, matching.go MatchingHandler twins), superseded helpers (router.parseUUID, handlers.ValidateDeviceToken, normalizePath, userExists, generateLockID) Templ/client: - never-routed pages: api_explorer.templ, queue.templ and their client modules api-explorer.ts, api-explorer-docs.ts, queue.ts - fully orphaned wrapper modules events.ts, dom.ts - broken clearSyncQueue (called a nonexistent endpoint) and the hidden, never-populated sync-queue section on the devices page - 166+ unused exports demoted to private (knip-verified); ~85 truly dead declarations removed via tsc noUnusedLocals fixpoint - stale 'interactive API Explorer' claims dropped from docs The docs drift-gate package (internal/docs), templ-bound modules (dashboard, stats, header, index, login, profile*, bookshelf, collections, toast-error), sqlc-generated code, and web/static are intentionally untouched.
169 lines
4.1 KiB
Go
169 lines
4.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
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 := RateLimiterConfig{
|
|
Enabled: true,
|
|
RequestsPerMinute: 60,
|
|
CleanupInterval: time.Minute,
|
|
}
|
|
limiter := NewRateLimiter(config)
|
|
|
|
assert.NotNil(t, limiter)
|
|
}
|