Neues Paket mail/internal/ratelimit: Token-Bucket je (Mandant, Quelle)-Schlüssel. ConfigProvider/StaticConfig liefern Burst/Nachfüllrate je Mandant mit Fallback auf eine Default-Konfiguration (Akzeptanzkriterium 2/3: begrenzt, konfigurierbar, je Mandant getrennt). Limiter.Allow liefert bei Ablehnung eine konkrete positive Wartezeit statt nur false — Grundlage für die geforderte definierte Ablehnung MIT Wartezeit-Hinweis statt Verbindungsabbruch (Akzeptanzkriterium 3). IMAP LOGIN und POP3 PASS begrenzen Anmeldeversuche pro (Mandant, Quell-IP). SMTP MAIL FROM begrenzt die Annahmerate pro (Mandant, Absenderadresse+Quell-IP). Bei Überschreitung: POP3 -ERR, IMAP NO, SMTP 451 (temporärer Fehlercode) mit Wartezeit in Sekunden, Verbindung bleibt in allen drei Fällen offen und weiter nutzbar. loginLimiter/ acceptLimiter sind optional (nil = deaktiviert, Rückwärtskompatibilität zu ING-01..ING-08) — neue Konstruktoren NewServerWithGuardTLSLoggerAndRateLimit (IMAP/POP3) und NewServerWithMaxMessageBytesTLSLoggerAndRateLimit (SMTP). Jeder Server bekommt eine tenantID, konsistent mit dem in ING-10 etablierten Muster "ein Server je Mandant". Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt, jeweils in IMAP, POP3 und SMTP einzeln: 20 reale Versuche bei Burst=5 -> exakt 5 akzeptiert, 15 mit Wartezeit-Hinweis abgelehnt, keine Verbindung getrennt; legitime Nutzung unterhalb der Schwelle unbeeinträchtigt; zwei Mandanten mit unterschiedlichem Burst über denselben Limiter unabhängig und exakt wirksam (2 vs. 8 akzeptierte Versuche). go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~31 Pakete) regressionsfrei getestet — insbesondere QA-07-Lasttests bleiben grün, da Rate-Limiting standardmäßig deaktiviert ist.
71 lines
2.0 KiB
Go
71 lines
2.0 KiB
Go
package ratelimit
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLimiter_AllowsBurstThenBlocksThenRecovers(t *testing.T) {
|
|
cfg := Config{Burst: 3, RefillEvery: 50 * time.Millisecond}
|
|
lim := NewLimiter(StaticConfig{Default: cfg})
|
|
|
|
for i := 0; i < 3; i++ {
|
|
ok, _ := lim.Allow("mandant-a", "1.2.3.4")
|
|
if !ok {
|
|
t.Fatalf("versuch %d im burst hätte erlaubt sein müssen", i+1)
|
|
}
|
|
}
|
|
ok, retryAfter := lim.Allow("mandant-a", "1.2.3.4")
|
|
if ok {
|
|
t.Fatalf("vierter versuch über dem burst hätte abgelehnt werden müssen")
|
|
}
|
|
if retryAfter <= 0 {
|
|
t.Fatalf("erwartete positive wartezeit als hinweis, habe %v", retryAfter)
|
|
}
|
|
|
|
time.Sleep(retryAfter + 10*time.Millisecond)
|
|
ok, _ = lim.Allow("mandant-a", "1.2.3.4")
|
|
if !ok {
|
|
t.Fatalf("nach der wartezeit hätte wieder ein token verfügbar sein müssen")
|
|
}
|
|
}
|
|
|
|
func TestLimiter_TenantsAreIndependent(t *testing.T) {
|
|
lim := NewLimiter(StaticConfig{Default: Config{Burst: 1, RefillEvery: time.Hour}})
|
|
|
|
okA, _ := lim.Allow("mandant-a", "1.2.3.4")
|
|
if !okA {
|
|
t.Fatalf("mandant a: erster versuch hätte erlaubt sein müssen")
|
|
}
|
|
okA2, _ := lim.Allow("mandant-a", "1.2.3.4")
|
|
if okA2 {
|
|
t.Fatalf("mandant a: zweiter versuch hätte abgelehnt werden müssen")
|
|
}
|
|
|
|
// Mandant B mit DERSELBEN Quelladresse — eigener Bucket.
|
|
okB, _ := lim.Allow("mandant-b", "1.2.3.4")
|
|
if !okB {
|
|
t.Fatalf("mandant b: eigener bucket, erster versuch hätte erlaubt sein müssen")
|
|
}
|
|
}
|
|
|
|
func TestLimiter_PerTenantConfigOverridesDefault(t *testing.T) {
|
|
lim := NewLimiter(StaticConfig{
|
|
Default: Config{Burst: 1, RefillEvery: time.Hour},
|
|
PerTenant: map[string]Config{
|
|
"mandant-grosszuegig": {Burst: 5, RefillEvery: time.Hour},
|
|
},
|
|
})
|
|
|
|
for i := 0; i < 5; i++ {
|
|
ok, _ := lim.Allow("mandant-grosszuegig", "1.2.3.4")
|
|
if !ok {
|
|
t.Fatalf("mandant-grosszuegig: versuch %d hätte im eigenen, größeren burst erlaubt sein müssen", i+1)
|
|
}
|
|
}
|
|
ok, _ := lim.Allow("mandant-grosszuegig", "1.2.3.4")
|
|
if ok {
|
|
t.Fatalf("mandant-grosszuegig: sechster versuch hätte abgelehnt werden müssen")
|
|
}
|
|
}
|