Files
OwnCord/Server/auth/ratelimit_test.go
T
jevb 9c1d99683c fix: address PR review findings (issues #9-#14)
- Fix capacity over-allocation and use strings.Builder in getReactionsBatch (#9)
- Replace `any` types and cache Tauri invoke in window-state.ts (#10)
- Remove custom `contains` helper, fix NilHub tests to pass nil (#11)
- Add nil guards before hub method calls in admin handlers (#12)
- Run golangci-lint v2: modernize interface{}/any, range-over-int loops,
  remove dead code, fix errcheck, add .golangci.yml config (#13)
- Add 23 client unit test suites (694 tests), exclude Tauri-coupled
  files from coverage, achieve 80%+ threshold (#14)

Closes #9, closes #10, closes #11, closes #12, closes #13, closes #14
2026-03-17 04:11:04 +01:00

127 lines
3.1 KiB
Go

package auth_test
import (
"testing"
"time"
"github.com/owncord/server/auth"
)
func TestRateLimiter_UnderLimitAllowed(t *testing.T) {
rl := auth.NewRateLimiter()
for i := range 5 {
if !rl.Allow("key1", 5, time.Second) {
t.Errorf("Allow() = false at iteration %d, want true", i)
}
}
}
func TestRateLimiter_AtLimitAllowed(t *testing.T) {
rl := auth.NewRateLimiter()
// Allow up to exactly the limit
for range 3 {
rl.Allow("keyA", 3, time.Second)
}
// The 4th call should be blocked
if rl.Allow("keyA", 3, time.Second) {
t.Error("Allow() = true after limit exceeded, want false")
}
}
func TestRateLimiter_OverLimitBlocked(t *testing.T) {
rl := auth.NewRateLimiter()
limit := 3
for range limit {
rl.Allow("key2", limit, time.Second)
}
if rl.Allow("key2", limit, time.Second) {
t.Error("Allow() = true when over limit, want false")
}
}
func TestRateLimiter_WindowExpiryResets(t *testing.T) {
rl := auth.NewRateLimiter()
window := 50 * time.Millisecond
limit := 2
// Exhaust limit
rl.Allow("key3", limit, window)
rl.Allow("key3", limit, window)
if rl.Allow("key3", limit, window) {
t.Error("Allow() should be blocked after exhausting limit")
}
// Wait for window to expire
time.Sleep(window + 10*time.Millisecond)
if !rl.Allow("key3", limit, window) {
t.Error("Allow() should be permitted after window expires")
}
}
func TestRateLimiter_DifferentKeysIndependent(t *testing.T) {
rl := auth.NewRateLimiter()
for range 5 {
rl.Allow("keyX", 3, time.Second)
}
// keyY should still be allowed
if !rl.Allow("keyY", 3, time.Second) {
t.Error("Allow() blocked keyY even though only keyX exceeded limit")
}
}
func TestRateLimiter_LockoutEnforced(t *testing.T) {
rl := auth.NewRateLimiter()
rl.Lockout("keyLock", time.Hour)
if !rl.IsLockedOut("keyLock") {
t.Error("IsLockedOut() = false after Lockout(), want true")
}
}
func TestRateLimiter_LockoutExpires(t *testing.T) {
rl := auth.NewRateLimiter()
rl.Lockout("keyExp", 30*time.Millisecond)
time.Sleep(50 * time.Millisecond)
if rl.IsLockedOut("keyExp") {
t.Error("IsLockedOut() = true after lockout expired, want false")
}
}
func TestRateLimiter_IsLockedOut_UnknownKey(t *testing.T) {
rl := auth.NewRateLimiter()
if rl.IsLockedOut("unknown") {
t.Error("IsLockedOut() = true for unknown key, want false")
}
}
func TestRateLimiter_Reset(t *testing.T) {
rl := auth.NewRateLimiter()
rl.Allow("keyR", 1, time.Second)
rl.Allow("keyR", 1, time.Second) // now blocked
rl.Reset("keyR")
if !rl.Allow("keyR", 1, time.Second) {
t.Error("Allow() = false after Reset(), want true")
}
}
func TestRateLimiter_LockoutBlocksAllow(t *testing.T) {
rl := auth.NewRateLimiter()
rl.Lockout("keyLB", time.Hour)
// Even under normal limit, lockout should block
if rl.Allow("keyLB", 100, time.Second) {
t.Error("Allow() = true for locked-out key, want false")
}
}
func TestRateLimiter_ThreadSafe(t *testing.T) {
rl := auth.NewRateLimiter()
done := make(chan struct{}, 100)
for range 100 {
go func() {
rl.Allow("concurrent", 50, time.Second)
done <- struct{}{}
}()
}
for range 100 {
<-done
}
// If we get here without a race condition data race, we pass
}