// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package ratelimit import ( "testing" "time" ) // clockAt pins the limiter to a fake clock so refill is a function of arithmetic, not sleeping. func clockAt(l *Limiter) *time.Time { t := time.Unix(1000, 0) l.now = func() time.Time { return t } return &t } func TestBurstThenRefusalThenRefill(t *testing.T) { l := New(1, 3) // 1 token/s, burst 3 now := clockAt(l) for i := 0; i < 3; i++ { if ok, _ := l.Allow("k"); !ok { t.Fatalf("token %d of the burst refused", i) } } ok, wait := l.Allow("k") if ok { t.Fatal("fourth token inside the same instant should be refused") } if wait <= 0 || wait > time.Second { t.Fatalf("retry-after = %v, want (0, 1s]", wait) } *now = now.Add(2 * time.Second) // refills 2 tokens if ok, _ := l.Allow("k"); !ok { t.Fatal("refused after refill") } if ok, _ := l.Allow("k"); !ok { t.Fatal("second refilled token refused") } if ok, _ := l.Allow("k"); ok { t.Fatal("third token allowed but only two seconds elapsed") } } func TestRefusalConsumesNothing(t *testing.T) { l := New(1, 1) now := clockAt(l) l.Allow("k") // Hammering while empty must not push the refill out. for i := 0; i < 10; i++ { if ok, _ := l.Allow("k"); ok { t.Fatal("allowed while empty") } } *now = now.Add(time.Second) if ok, _ := l.Allow("k"); !ok { t.Fatal("the refused attempts ate the refill") } } func TestKeysAreIndependent(t *testing.T) { l := New(1, 1) clockAt(l) l.Allow("a") if ok, _ := l.Allow("b"); !ok { t.Fatal("draining key a refused key b") } } func TestAllowNChargesBytes(t *testing.T) { l := New(1000, 1000) // e.g. bytes/s clockAt(l) if ok, _ := l.AllowN("k", 900); !ok { t.Fatal("900 of 1000 refused") } if ok, _ := l.AllowN("k", 200); ok { t.Fatal("1100 of 1000 allowed") } if ok, _ := l.AllowN("k", 100); !ok { t.Fatal("the refused 200 consumed the remaining 100") } } func TestNilLimiterAllowsEverything(t *testing.T) { var l *Limiter if ok, wait := l.AllowN("k", 1e12); !ok || wait != 0 { t.Fatal("nil limiter must be a no-op") } } func TestSweepDropsIdleBucketsOnly(t *testing.T) { l := New(1, 3) now := clockAt(l) l.Allow("idle") *now = now.Add(2 * time.Minute) l.Allow("busy") // triggers the sweep; "idle" refilled long ago if _, held := l.buckets["idle"]; held { t.Fatal("idle bucket survived the sweep") } if _, held := l.buckets["busy"]; !held { t.Fatal("active bucket was swept") } }