mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Add comprehensive tests across all Go packages: - auth: username validation, concurrent rate limiting, TOTP stores, timing - config: env overrides, default credential detection, voice defaults - db: search, message queries, special char handling - api: handler edge cases, error paths, DM/invite/TOTP coverage - ws: voice handler paths, integration scenarios - updater: version comparison, timeout handling 6 of 8 packages now at 80%+ coverage.
233 lines
6.2 KiB
Go
233 lines
6.2 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
|
|
}
|
|
|
|
// ─── Check (read-only rate-limit query) ─────────────────────────────────────
|
|
|
|
func TestRateLimiter_Check_UnderLimit(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
// No requests recorded yet — Check should return true.
|
|
if !rl.Check("checkKey", 5, time.Second) {
|
|
t.Error("Check() = false for fresh key, want true")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiter_Check_DoesNotRecordTimestamp(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
// Call Check many times — it must NOT record timestamps.
|
|
for range 10 {
|
|
rl.Check("checkKey2", 3, time.Second)
|
|
}
|
|
// Allow should still succeed because Check didn't record anything.
|
|
if !rl.Allow("checkKey2", 3, time.Second) {
|
|
t.Error("Allow() = false after only Check() calls, want true")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiter_Check_AtLimit(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
// Record exactly 3 requests via Allow.
|
|
for range 3 {
|
|
rl.Allow("checkKey3", 3, time.Second)
|
|
}
|
|
// Check should report the key is at/over limit.
|
|
if rl.Check("checkKey3", 3, time.Second) {
|
|
t.Error("Check() = true when at limit, want false")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiter_Check_RespectsLockout(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
rl.Lockout("checkLocked", time.Hour)
|
|
if rl.Check("checkLocked", 100, time.Second) {
|
|
t.Error("Check() = true for locked-out key, want false")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiter_Check_LockoutExpired(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
rl.Lockout("checkExpLock", 10*time.Millisecond)
|
|
time.Sleep(30 * time.Millisecond)
|
|
if !rl.Check("checkExpLock", 5, time.Second) {
|
|
t.Error("Check() = false after lockout expired, want true")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiter_Check_WindowBoundary(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
window := 50 * time.Millisecond
|
|
// Exhaust limit.
|
|
for range 3 {
|
|
rl.Allow("checkBound", 3, window)
|
|
}
|
|
if rl.Check("checkBound", 3, window) {
|
|
t.Error("Check() = true at limit, want false")
|
|
}
|
|
// Wait for window to expire.
|
|
time.Sleep(window + 20*time.Millisecond)
|
|
if !rl.Check("checkBound", 3, window) {
|
|
t.Error("Check() = false after window expired, want true")
|
|
}
|
|
}
|
|
|
|
// ─── Concurrent hammering ───────────────────────────────────────────────────
|
|
|
|
func TestRateLimiter_ConcurrentHammering(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
limit := 10
|
|
window := time.Second
|
|
allowed := make(chan bool, 200)
|
|
|
|
for range 200 {
|
|
go func() {
|
|
allowed <- rl.Allow("hammer", limit, window)
|
|
}()
|
|
}
|
|
|
|
trueCount := 0
|
|
for range 200 {
|
|
if <-allowed {
|
|
trueCount++
|
|
}
|
|
}
|
|
// Exactly `limit` requests should be allowed.
|
|
if trueCount != limit {
|
|
t.Errorf("concurrent Allow() allowed %d requests, want exactly %d", trueCount, limit)
|
|
}
|
|
}
|
|
|
|
func TestRateLimiter_ResetClearsLockout(t *testing.T) {
|
|
rl := auth.NewRateLimiter()
|
|
rl.Lockout("resetLock", time.Hour)
|
|
if !rl.IsLockedOut("resetLock") {
|
|
t.Fatal("precondition: key should be locked out")
|
|
}
|
|
rl.Reset("resetLock")
|
|
if rl.IsLockedOut("resetLock") {
|
|
t.Error("Reset() should clear lockout, but key is still locked out")
|
|
}
|
|
}
|