package calls import ( "testing" "time" ) // Task 5 review minor 5: unit coverage for ratelimit.go's own behaviour, // independent of the signal endpoint that happens to be its only caller. func TestRateLimiterAllowsUpToBurstThenRefuses(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) l := newRateLimiter(30, 3, 0) for i := 0; i < 3; i++ { if ok, _ := l.allow("k", now); !ok { t.Fatalf("call %d within burst was refused", i) } } ok, retry := l.allow("k", now) if ok { t.Fatal("a call past the burst, with no time elapsed, was allowed") } if retry <= 0 { t.Fatalf("retryAfter %v, want > 0", retry) } } func TestRateLimiterRefillsAfterAnExplicitElapsedDuration(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) l := newRateLimiter(30, 1, 0) if ok, _ := l.allow("k", now); !ok { t.Fatal("the first call, at burst, was refused") } if ok, _ := l.allow("k", now); ok { t.Fatal("a second call at the same instant was allowed with an empty bucket") } // One token at 30/s refills in 1/30 s ≈ 33 ms; 40 ms is comfortably // past that. later := now.Add(40 * time.Millisecond) if ok, _ := l.allow("k", later); !ok { t.Fatal("a call after the refill window was still refused") } } func TestRateLimiterRetryAfterIsBoundedByOneTokensRefillTime(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) l := newRateLimiter(30, 1, 0) l.allow("k", now) _, retry := l.allow("k", now) oneToken := time.Second / 30 if retry <= 0 || retry > oneToken+time.Millisecond { t.Fatalf("retryAfter %v, want in (0, %v]", retry, oneToken) } } func TestRateLimiterEvictsABucketIdleOverAMinute(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) l := newRateLimiter(30, 60, 0) l.allow("k", now) l.mu.Lock() l.sweepLocked(now.Add(59 * time.Second)) _, stillThereAt59s := l.buckets["k"] l.sweepLocked(now.Add(61 * time.Second)) _, stillThereAt61s := l.buckets["k"] l.mu.Unlock() if !stillThereAt59s { t.Fatal("a bucket under a minute idle was swept early") } if stillThereAt61s { t.Fatal("a bucket over a minute idle was not swept") } } func TestRateLimiterCapEvictsTheLeastRecentlyTouchedBucket(t *testing.T) { now := time.Date(2026, 9, 10, 9, 0, 0, 0, time.UTC) l := newRateLimiter(30, 60, 2) l.allow("a", now) l.allow("b", now.Add(time.Second)) l.mu.Lock() n := len(l.buckets) l.mu.Unlock() if n != 2 { t.Fatalf("buckets after two distinct keys: %d, want 2", n) } // A third key must not grow the map past the cap: "a", the oldest, // is evicted to make room. l.allow("c", now.Add(2*time.Second)) l.mu.Lock() _, aStillThere := l.buckets["a"] _, bStillThere := l.buckets["b"] _, cThere := l.buckets["c"] size := len(l.buckets) l.mu.Unlock() if size > 2 { t.Fatalf("bucket count %d exceeds the cap of 2", size) } if aStillThere { t.Fatal("the least recently touched bucket ('a') was not evicted to make room") } if !bStillThere || !cThere { t.Fatal("a bucket newer than the one evicted was lost instead") } }