package web import ( "fmt" "testing" "time" ) func TestLimiterAllowsFirstAttempts(t *testing.T) { l := NewLimiter(nil) for i := 0; i < 3; i++ { if _, ok := l.Allow("mmueller", "10.0.0.1"); !ok { t.Fatalf("Versuch %d muss erlaubt sein", i+1) } l.RecordFailure("mmueller", "10.0.0.1") } if _, ok := l.Allow("mmueller", "10.0.0.1"); ok { t.Fatal("nach 3 Fehlversuchen muss gebremst werden") } } func TestBackoffGrowsAndExpires(t *testing.T) { now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) l := NewLimiter(func() time.Time { return now }) for i := 0; i < 4; i++ { l.RecordFailure("mmueller", "10.0.0.1") } wait1, ok := l.Allow("mmueller", "10.0.0.1") if ok { t.Fatal("nach 4 Fehlversuchen muss gebremst werden") } if wait1 <= 0 { t.Fatalf("retryAfter = %v, muss positiv sein", wait1) } // Wartezeit absitzen -> wieder erlaubt. now = now.Add(wait1 + time.Second) if _, ok := l.Allow("mmueller", "10.0.0.1"); !ok { t.Fatal("nach Ablauf der Wartezeit muss wieder erlaubt sein") } // Weiterer Fehlversuch -> längere Wartezeit als vorher. l.RecordFailure("mmueller", "10.0.0.1") wait2, ok := l.Allow("mmueller", "10.0.0.1") if ok { t.Fatal("erneuter Fehlversuch muss wieder bremsen") } if wait2 <= wait1 { t.Errorf("Backoff muss wachsen: %v -> %v", wait1, wait2) } } func TestBackoffIsCapped(t *testing.T) { now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) l := NewLimiter(func() time.Time { return now }) for i := 0; i < 40; i++ { l.RecordFailure("mmueller", "10.0.0.1") } wait, ok := l.Allow("mmueller", "10.0.0.1") if ok { t.Fatal("muss gebremst sein") } if wait > maxBackoff { t.Errorf("retryAfter = %v, Deckel ist %v", wait, maxBackoff) } } func TestIPLimitProtectsAgainstUserSpray(t *testing.T) { l := NewLimiter(nil) // Angreifer probiert viele verschiedene Benutzernamen von einer IP. for i := 0; i < 12; i++ { l.RecordFailure(fmt.Sprintf("user%d", i), "10.0.0.9") } if _, ok := l.Allow("nochNieProbiert", "10.0.0.9"); ok { t.Fatal("die IP muss trotz wechselnder Benutzernamen gebremst werden") } } func TestUserLimitProtectsAcrossIPs(t *testing.T) { l := NewLimiter(nil) // Verteilter Angriff auf einen Benutzer von vielen IPs. for i := 0; i < 12; i++ { l.RecordFailure("mmueller", fmt.Sprintf("10.0.1.%d", i)) } if _, ok := l.Allow("mmueller", "192.168.99.99"); ok { t.Fatal("der Benutzer muss auch von einer neuen IP gebremst werden") } } func TestUsernameIsCaseInsensitive(t *testing.T) { l := NewLimiter(nil) for i := 0; i < 6; i++ { l.RecordFailure("MMueller", "10.0.0.1") } // Andere Schreibweise darf den Zähler nicht umgehen. if _, ok := l.Allow("mmueller", "192.168.5.5"); ok { t.Fatal("Schreibweise des Benutzernamens darf das Limit nicht umgehen") } } func TestSuccessResetsBothCounters(t *testing.T) { l := NewLimiter(nil) for i := 0; i < 5; i++ { l.RecordFailure("mmueller", "10.0.0.1") } if _, ok := l.Allow("mmueller", "10.0.0.1"); ok { t.Fatal("Vorbedingung: muss gebremst sein") } l.RecordSuccess("mmueller", "10.0.0.1") if _, ok := l.Allow("mmueller", "10.0.0.1"); !ok { t.Fatal("erfolgreiche Anmeldung muss die Zähler zurücksetzen") } } func TestNoHardLockout(t *testing.T) { now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) l := NewLimiter(func() time.Time { return now }) for i := 0; i < 100; i++ { l.RecordFailure("opfer", "10.0.0.1") } // Nach dem gedeckelten Backoff darf der Benutzer wieder anklopfen — // sonst könnte ein Dritter Kollegen dauerhaft aussperren. now = now.Add(maxBackoff + time.Second) if _, ok := l.Allow("opfer", "10.0.0.1"); !ok { t.Fatal("es darf keinen dauerhaften Lockout geben") } } func TestGCDropsIdleEntries(t *testing.T) { now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) l := NewLimiter(func() time.Time { return now }) l.RecordFailure("mmueller", "10.0.0.1") now = now.Add(2 * entryTTL) l.GC() if n := l.size(); n != 0 { t.Fatalf("GC muss veraltete Einträge entfernen, size = %d", n) } }