package clientip import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) func TestExtractSourceIP(t *testing.T) { t.Parallel() cases := []struct { name string header string remoteAddr string want string }{ { name: "single XFF entry trimmed", header: " 198.51.100.7 ", remoteAddr: "10.0.0.1:5000", want: "198.51.100.7", }, { name: "first XFF entry wins", header: "198.51.100.7, 10.0.0.1, 192.168.1.1", remoteAddr: "10.0.0.1:5000", want: "198.51.100.7", }, { name: "fallback to RemoteAddr without port", header: "", remoteAddr: "203.0.113.42:65000", want: "203.0.113.42", }, { name: "RemoteAddr without port preserved", header: "", remoteAddr: "203.0.113.42", want: "203.0.113.42", }, { name: "no header and no RemoteAddr returns empty", header: "", remoteAddr: "", want: "", }, } gin.SetMode(gin.TestMode) for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = tc.remoteAddr if tc.header != "" { req.Header.Set("X-Forwarded-For", tc.header) } c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = req got := ExtractSourceIP(c) if got != tc.want { t.Fatalf("ExtractSourceIP() = %q, want %q", got, tc.want) } }) } } func TestExtractSourceIPNilSafety(t *testing.T) { t.Parallel() if got := ExtractSourceIP(nil); got != "" { t.Fatalf("nil context: want empty, got %q", got) } c, _ := gin.CreateTestContext(httptest.NewRecorder()) if got := ExtractSourceIP(c); got != "" { t.Fatalf("context with nil Request: want empty, got %q", got) } }