package proxy_test import ( "net/http" "sync/atomic" "testing" "sneak.berlin/go/smallwebwaf/internal/requestlog" ) func TestRateLimitRefusesWith429BeforeTheApp(t *testing.T) { t.Parallel() var calls atomic.Int32 app := startApp(t, func(http.ResponseWriter, *http.Request) { calls.Add(1) }) addr, out := startProxy(t, app.URL, map[string]string{ trustedProxies: trustLocalhost, rateLimitPerMinute: "1", }) const otherClient = "203.0.113.10" // With a limit of one request a minute, a client's second request is // refused. A client is one IPv4 address, or one IPv6 /64; an IPv4 // address in IPv6 form is that IPv4 address. requests := []struct { client string // as X-Forwarded-For names it logged string // as the log line's client_ip names it want int }{ {client, client, http.StatusOK}, {client, client, http.StatusTooManyRequests}, {otherClient, otherClient, http.StatusOK}, {"::ffff:" + otherClient, otherClient, http.StatusTooManyRequests}, {"2001:db8::1", "2001:db8::1", http.StatusOK}, {"2001:db8::8000:0:0:1", "2001:db8::8000:0:0:1", http.StatusTooManyRequests}, {"2001:db8:0:1::1", "2001:db8:0:1::1", http.StatusOK}, } for i, sent := range requests { req := newRequest(t, http.MethodGet, addr, "/", http.NoBody) req.Header.Set(forwardedFor, sent.client) wantStatus(t, do(t, req), sent.want) line := out.requestLines(t, i+1)[i] if line.ClientIP != sent.logged { t.Errorf("log line has client_ip %q, want %q", line.ClientIP, sent.logged) } if sent.want == http.StatusOK { wantLine(t, line, http.StatusOK, requestlog.ActionForward) } else { wantLine(t, line, http.StatusTooManyRequests, requestlog.ActionRateLimited) if line.LimitHit != "minute" { t.Errorf("log line has limit_hit %q, want minute", line.LimitHit) } } } if calls.Load() != 4 { t.Errorf("the app was called %d times, want 4", calls.Load()) } }