package httpapi import ( "net/http" "net/http/httptest" "testing" ) func TestExtractClientIP(t *testing.T) { tests := []struct { name string request *http.Request headers map[string]string remoteAddr string wantIP *string }{ { name: "nil request", request: nil, wantIP: nil, }, { name: "CF-Connecting-IP takes precedence", headers: map[string]string{ "CF-Connecting-IP": "203.0.113.10", "X-Real-IP": "198.51.100.20", "X-Forwarded-For": "192.0.2.30", }, remoteAddr: "10.0.0.1:54321", wantIP: ptr("203.0.113.10"), }, { name: "True-Client-IP takes precedence over X-Real-IP", headers: map[string]string{ "True-Client-IP": "203.0.113.11", "X-Real-IP": "198.51.100.20", "X-Forwarded-For": "192.0.2.30", }, remoteAddr: "10.0.0.1:54321", wantIP: ptr("203.0.113.11"), }, { name: "X-Real-IP takes precedence over X-Forwarded-For", headers: map[string]string{ "X-Real-IP": "198.51.100.20", "X-Forwarded-For": "192.0.2.30", }, remoteAddr: "10.0.0.1:54321", wantIP: ptr("198.51.100.20"), }, { name: "X-Forwarded-For first entry is extracted", headers: map[string]string{ "X-Forwarded-For": "192.0.2.30, 10.0.0.2, 172.16.0.5", }, remoteAddr: "10.0.0.1:54321", wantIP: ptr("192.0.2.30"), }, { name: "X-Forwarded-For single entry", headers: map[string]string{ "X-Forwarded-For": " 192.0.2.31 ", }, remoteAddr: "10.0.0.1:54321", wantIP: ptr("192.0.2.31"), }, { name: "X-Forwarded-For skips invalid first entry to find valid IP", headers: map[string]string{ "X-Forwarded-For": "unknown, 192.0.2.32", }, remoteAddr: "10.0.0.1:54321", wantIP: ptr("192.0.2.32"), }, { name: "Fallback to RemoteAddr with port", headers: map[string]string{}, remoteAddr: "192.168.1.100:12345", wantIP: ptr("192.168.1.100"), }, { name: "Fallback to RemoteAddr without port", headers: map[string]string{}, remoteAddr: "192.168.1.101", wantIP: ptr("192.168.1.101"), }, { name: "IPv6 in RemoteAddr with port", headers: map[string]string{}, remoteAddr: "[2001:db8::1]:8080", wantIP: ptr("2001:db8::1"), }, { name: "IPv6 in header", headers: map[string]string{ "X-Real-IP": "2001:db8::2", }, remoteAddr: "10.0.0.1:54321", wantIP: ptr("2001:db8::2"), }, { name: "Invalid headers fall back to RemoteAddr", headers: map[string]string{ "X-Real-IP": "not-an-ip", "X-Forwarded-For": "also-not-an-ip", }, remoteAddr: "192.168.1.102:443", wantIP: ptr("192.168.1.102"), }, { name: "No headers and empty RemoteAddr", headers: map[string]string{}, remoteAddr: "", wantIP: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var req *http.Request if tt.name != "nil request" { req = httptest.NewRequest(http.MethodGet, "/", nil) for k, v := range tt.headers { req.Header.Set(k, v) } req.RemoteAddr = tt.remoteAddr } got := extractClientIP(req) if (got == nil) != (tt.wantIP == nil) { t.Fatalf("extractClientIP() = %v, want %v", got, tt.wantIP) } if got != nil && tt.wantIP != nil && *got != *tt.wantIP { t.Errorf("extractClientIP() = %q, want %q", *got, *tt.wantIP) } }) } } func ptr(s string) *string { return &s }