package smtp import ( "context" "net" "strings" "testing" "time" "gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit" ) func startRateLimitedServer(t *testing.T, sink MessageSink, tenant string, limiter *ratelimit.Limiter) (addr string, stop func()) { t.Helper() srv := NewServerWithMaxMessageBytesTLSLoggerAndRateLimit(sink, defaultMaxMessageBytes, 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 } } // attemptMailFrom führt EHLO+MAIL FROM über eine NEUE Verbindung aus // und liefert die MAIL FROM-Antwortzeile. func attemptMailFrom(t *testing.T, addr, from string) string { t.Helper() c := dial(t, addr) defer c.close() c.send(t, "EHLO client.example.com") for { line := c.readLine(t) if strings.HasPrefix(line, "250 ") { break } } return c.send(t, "MAIL FROM:<"+from+">") } // 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}, }) sink := &fakeSink{} addr, stop := startRateLimitedServer(t, sink, "mandant-a", limiter) defer stop() const attempts = 20 var accepted, rejected int for i := 0; i < attempts; i++ { resp := attemptMailFrom(t, addr, "immer-gleicher-absender@example.com") switch { case code(resp) == "250": accepted++ case code(resp) == "451" && strings.Contains(resp, "rate limit"): rejected++ default: t.Fatalf("unerwartete antwort: %q", resp) } } 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}, }) sink := &fakeSink{} addr, stop := startRateLimitedServer(t, sink, "mandant-a", limiter) defer stop() for i := 0; i < 3; i++ { resp := attemptMailFrom(t, addr, "legitim@example.com") if code(resp) != "250" { t.Fatalf("versuch %d unterhalb der schwelle wurde abgelehnt: %q", i+1, resp) } } } // 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}, }, }) sinkKnapp := &fakeSink{} addrKnapp, stopKnapp := startRateLimitedServer(t, sinkKnapp, "mandant-knapp", limiter) defer stopKnapp() sinkGross := &fakeSink{} addrGross, stopGross := startRateLimitedServer(t, sinkGross, "mandant-grosszuegig", limiter) defer stopGross() var acceptedKnapp int for i := 0; i < 10; i++ { if code(attemptMailFrom(t, addrKnapp, "absender@example.com")) == "250" { acceptedKnapp++ } } var acceptedGross int for i := 0; i < 10; i++ { if code(attemptMailFrom(t, addrGross, "absender@example.com")) == "250" { 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) } }