package imap import ( "context" "net" "strings" "testing" "time" "gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard" "gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit" ) func startRateLimitedServer(t *testing.T, tenant string, limiter *ratelimit.Limiter) (addr string, stop func()) { t.Helper() auth := fakeAuthenticator{users: map[string]string{"alice": "geheim123"}} store := fakeMailboxStore{mailboxes: map[string][]Message{ "INBOX": {{SequenceNumber: 1, UID: 1, Flags: []string{}}}, }} srv := NewServerWithGuardTLSLoggerAndRateLimit(auth, store, protoguard.DefaultConfig(), nil, nil, tenant, limiter) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listener: %v", err) } ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { _ = srv.Serve(ctx, listener) close(done) }() return listener.Addr().String(), func() { cancel() <-done } } // attemptLogin führt LOGIN über eine NEUE Verbindung aus und liefert // die Abschlusszeile. func attemptLogin(t *testing.T, addr string) string { t.Helper() c := dial(t, addr) defer c.close() _, lines := c.sendTagged(t, "LOGIN alice geheim123") return lines[len(lines)-1] } // TestRateLimit_LoadExceedingLimitGetsRejectedWithRetryHint ist die // geforderte Pflichtprüfung 1 (ING-09). func TestRateLimit_LoadExceedingLimitGetsRejectedWithRetryHint(t *testing.T) { limiter := ratelimit.NewLimiter(ratelimit.StaticConfig{ Default: ratelimit.Config{Burst: 5, RefillEvery: time.Hour}, }) addr, stop := startRateLimitedServer(t, "mandant-a", limiter) defer stop() const attempts = 20 var accepted, rejected int for i := 0; i < attempts; i++ { last := attemptLogin(t, addr) switch { case strings.Contains(last, "OK"): accepted++ case strings.Contains(last, "NO") && strings.Contains(last, "rate limit"): rejected++ default: t.Fatalf("unerwartete abschlussantwort: %q", last) } } if accepted != 5 { t.Fatalf("erwartete genau 5 akzeptierte versuche (burst), habe %d", accepted) } if rejected != attempts-5 { t.Fatalf("erwartete %d abgelehnte versuche, habe %d", attempts-5, rejected) } } // TestRateLimit_LegitUsageBelowThresholdUnaffected ist die geforderte // Pflichtprüfung 2 (ING-09). func TestRateLimit_LegitUsageBelowThresholdUnaffected(t *testing.T) { limiter := ratelimit.NewLimiter(ratelimit.StaticConfig{ Default: ratelimit.Config{Burst: 10, RefillEvery: time.Second}, }) addr, stop := startRateLimitedServer(t, "mandant-a", limiter) defer stop() for i := 0; i < 3; i++ { last := attemptLogin(t, addr) if !strings.Contains(last, "OK") { t.Fatalf("versuch %d unterhalb der schwelle wurde abgelehnt: %q", i+1, last) } } } // TestRateLimit_PerTenantIndependentAndEffective ist die geforderte // Pflichtprüfung 3 (ING-09). func TestRateLimit_PerTenantIndependentAndEffective(t *testing.T) { limiter := ratelimit.NewLimiter(ratelimit.StaticConfig{ Default: ratelimit.Config{Burst: 2, RefillEvery: time.Hour}, PerTenant: map[string]ratelimit.Config{ "mandant-grosszuegig": {Burst: 8, RefillEvery: time.Hour}, }, }) addrKnapp, stopKnapp := startRateLimitedServer(t, "mandant-knapp", limiter) defer stopKnapp() addrGross, stopGross := startRateLimitedServer(t, "mandant-grosszuegig", limiter) defer stopGross() var acceptedKnapp int for i := 0; i < 10; i++ { if strings.Contains(attemptLogin(t, addrKnapp), "OK") { acceptedKnapp++ } } var acceptedGross int for i := 0; i < 10; i++ { if strings.Contains(attemptLogin(t, addrGross), "OK") { acceptedGross++ } } if acceptedKnapp != 2 { t.Fatalf("mandant-knapp: erwartete 2 akzeptierte versuche, habe %d", acceptedKnapp) } if acceptedGross != 8 { t.Fatalf("mandant-grosszuegig: erwartete 8 akzeptierte versuche, habe %d", acceptedGross) } }