Files
summercms/surf/limiter_coverage_test.go
Jakub Zych 6852a8f8c3 fix(06-07): make limiter admission atomic
- Replace split store checks with one mutex-guarded Attempt operation
- Use domainless trusted-client keys for anonymous inline throttles
- Preserve fixed-window headers, expiry, stacking, and principal isolation
2026-09-20 17:02:53 +02:00

93 lines
2.8 KiB
Go

package surf
import (
"net/http"
"net/http/httptest"
"net/netip"
"os"
"path/filepath"
"testing"
"time"
"git.golem15.com/golem15/summercms/compass"
)
// Gap (b): MemoryStore's sweep goroutine (purge), not Attempt's lazy expiry.
// Construct with a short sweep and assert the internal map drops
// an expired entry without a later Attempt.
func TestMemoryStoreSweepRemovesExpiredEntry(t *testing.T) {
s := NewMemoryStore(15 * time.Millisecond)
t.Cleanup(func() { close(s.stop) })
s.Attempt("k", 1, 25*time.Millisecond)
s.mu.Lock()
n := len(s.entries)
s.mu.Unlock()
if n != 1 {
t.Fatalf("after Attempt, entries = %d", n)
}
deadline := time.Now().Add(200 * time.Millisecond)
for time.Now().Before(deadline) {
s.mu.Lock()
n = len(s.entries)
s.mu.Unlock()
if n == 0 {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("sweep did not drop expired entry, count=%d", n)
}
func TestMemoryStoreAttemptRetryAfterAndExpiry(t *testing.T) {
s := NewMemoryStore(0)
allowed, attempts, retryAfter := s.Attempt("k", 1, 20*time.Millisecond)
if !allowed || attempts != 1 || retryAfter <= 0 {
t.Fatalf("first attempt = allowed %v, attempts %d, retryAfter %s", allowed, attempts, retryAfter)
}
allowed, attempts, retryAfter = s.Attempt("k", 1, 20*time.Millisecond)
if allowed || attempts != 1 || retryAfter <= 0 {
t.Fatalf("denied attempt = allowed %v, attempts %d, retryAfter %s", allowed, attempts, retryAfter)
}
time.Sleep(30 * time.Millisecond)
allowed, attempts, retryAfter = s.Attempt("k", 1, 20*time.Millisecond)
if !allowed || attempts != 1 || retryAfter <= 0 {
t.Fatalf("expired attempt = allowed %v, attempts %d, retryAfter %s", allowed, attempts, retryAfter)
}
}
func TestTrustedProxiesParsesConfig(t *testing.T) {
if TrustedProxies(nil) != nil {
t.Fatal("nil cfg must return nil")
}
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: t\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "http.yaml"), []byte("trusted_proxies:\n - 10.0.0.0/8\n - not-a-cidr\n - 192.168.0.0/16\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{}})
if err != nil {
t.Fatal(err)
}
got := TrustedProxies(cfg)
if len(got) != 2 || got[0].String() != "10.0.0.0/8" || got[1].String() != "192.168.0.0/16" {
t.Fatalf("TrustedProxies = %v", got)
}
}
func TestClientIPNilRequestAndEmptyXFF(t *testing.T) {
if got := ClientIP(nil, nil); got != "" {
t.Fatalf("nil request = %q", got)
}
trusted := []netip.Prefix{mustPrefix("10.0.0.0/8")}
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = "10.0.0.1:443"
if got := ClientIP(req, trusted); got != "10.0.0.1" {
t.Fatalf("trusted RemoteAddr, empty XFF = %q", got)
}
}