mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Fixes all 109 golangci-lint findings (106 contextcheck, 1 gocritic,
2 gosec) that accumulated after D2 wired dbgen (whose queries take ctx)
under ctx-less db.DB wrappers while CI lint was quota-dead. No nolint
comments added; every finding fixed by genuinely threading context.
- db: all 138 hand-written db.DB methods take ctx first; the dbCtx()
Background shim is deleted; raw Query/QueryRow/Exec/Begin use their
Context variants; the four redundant ctx-less passthroughs removed.
db.Auditor/WriteAudit gain ctx.
- Seams: permissions.Checker (DB iface, HasChannelPerm,
RequireChannelAccess) and the service.Store interface mirror the new
signatures (ws.EventStore and plugin.PluginStore already did).
- Callers: api/admin handlers use r.Context(); ws per-message paths use
the connection ctx via DispatchV2; hub loops and startup wiring use
context.Background(); service methods thread ctx where they have one
and Background where no ctx exists. Public service surface reached by
ctx-holding chains (PermissionService.HasChannelPerm/GetRoleForUser/
RequireChannelAccess, message/dm/block/invite/profile methods) is now
ctx-first.
- Detached (context.WithoutCancel) where cancellation would break an
invariant, found by a 3-lens adversarial review of the diff:
* voice-leave background retries (a dead webhook/connection ctx killed
retry 2 before it ran, leaving ghost capacity-holding voice rows)
* rollbackVoiceJoin's compensating delete (its trigger IS the cancel)
* post-2FA-change DeleteOtherSessions and logout DeleteSession (the
security tail of a committed change must not die with the request)
* all api/ws audit writes (a banned user could suppress their own
login_blocked_banned row by aborting the request mid-bcrypt)
* admin backup VACUUM INTO (an interrupt left a truncated .db that
the backup list presented as restorable)
* post-commit message/edit refetches (a committed message must still
fan out when the sender disconnects)
* hub settings-cache refresh (one dead connection could pin stale
values for the 30s TTL)
- gocritic rangeValCopy fixed (index iteration); gosec G306 excluded in
config with justification (generated source must stay world-readable)
instead of flipping genprotocol output to 0o600.
Verified: gofmt/vet, all four build-tag variants, full suite, deadlock
pass, full -race pass, golangci-lint 0 issues uncapped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
package auth_test
|
|
|
|
import (
|
|
"context"
|
|
"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(context.Background(), "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(context.Background(), "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(context.Background(), "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(context.Background(), "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(context.Background(), "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(context.Background(), "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(context.Background(), "resetLock", time.Hour)
|
|
if !rl.IsLockedOut("resetLock") {
|
|
t.Fatal("precondition: key should be locked out")
|
|
}
|
|
rl.Reset(context.Background(), "resetLock")
|
|
if rl.IsLockedOut("resetLock") {
|
|
t.Error("Reset() should clear lockout, but key is still locked out")
|
|
}
|
|
}
|