package httpapi import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "ipecho/internal/clientip" ) func TestIPEndpointReturnsPlainTextClientIP(t *testing.T) { handler := New(clientip.Resolver{}) req := httptest.NewRequest(http.MethodGet, "/ip", nil) req.RemoteAddr = "198.51.100.20:54123" rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want %d", rec.Code, http.StatusOK) } if got, want := rec.Body.String(), "198.51.100.20\n"; got != want { t.Fatalf("got body %q, want %q", got, want) } if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "text/plain") { t.Fatalf("got content-type %q, want text/plain", got) } } func TestRootReturnsPlainTextClientIPForCLIRequests(t *testing.T) { handler := New(clientip.Resolver{}) req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "198.51.100.20:54123" req.Header.Set("Accept", "*/*") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want %d", rec.Code, http.StatusOK) } if got, want := rec.Body.String(), "198.51.100.20\n"; got != want { t.Fatalf("got body %q, want %q", got, want) } if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "text/plain") { t.Fatalf("got content-type %q, want text/plain", got) } } func TestRootReturnsHTMLForBrowserRequests(t *testing.T) { handler := New(clientip.Resolver{}) req := httptest.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "198.51.100.20:54123" req.Header.Set("Accept", "text/html,application/xhtml+xml") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want %d", rec.Code, http.StatusOK) } if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "text/html") { t.Fatalf("got content-type %q, want text/html", got) } body := rec.Body.String() for _, want := range []string{"IP Echo", "198.51.100.20", "Copy IP", "/json"} { if !strings.Contains(body, want) { t.Fatalf("body does not contain %q", want) } } } func TestJSONEndpointReturnsRequestMetadata(t *testing.T) { handler := New(clientip.Resolver{TrustProxyHeaders: true}) req := httptest.NewRequest(http.MethodGet, "/json", nil) req.RemoteAddr = "198.51.100.20:54123" req.Header.Set("User-Agent", "ipecho-test") req.Header.Set("X-Forwarded-For", "203.0.113.10") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want %d", rec.Code, http.StatusOK) } var got ClientInfo if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("decode response: %v", err) } if got.IP != "203.0.113.10" { t.Fatalf("got ip %q, want %q", got.IP, "203.0.113.10") } if got.UserAgent != "ipecho-test" { t.Fatalf("got user agent %q, want %q", got.UserAgent, "ipecho-test") } } func TestHeadersEndpointIncludesHostAndHeaders(t *testing.T) { handler := New(clientip.Resolver{}) req := httptest.NewRequest(http.MethodGet, "/headers", nil) req.Host = "example.test" req.Header.Set("User-Agent", "ipecho-test") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) body := rec.Body.String() for _, want := range []string{"Host: example.test", "User-Agent: ipecho-test"} { if !strings.Contains(body, want) { t.Fatalf("body %q does not contain %q", body, want) } } } func TestFaviconEndpointReturnsIconWithoutHTMLLink(t *testing.T) { handler := New(clientip.Resolver{}) req := httptest.NewRequest(http.MethodGet, "/favicon.ico", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want %d", rec.Code, http.StatusOK) } if got := rec.Header().Get("Content-Type"); got != "image/x-icon" { t.Fatalf("got content-type %q, want image/x-icon", got) } if rec.Body.Len() == 0 { t.Fatal("favicon response is empty") } } func TestSecurityHeadersAreApplied(t *testing.T) { handler := New(clientip.Resolver{}) req := httptest.NewRequest(http.MethodGet, "/healthz", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) for _, header := range []string{ "X-Content-Type-Options", "X-Frame-Options", "Referrer-Policy", "Content-Security-Policy", } { if rec.Header().Get(header) == "" { t.Fatalf("missing %s header", header) } } } func TestUnknownEndpointReturnsNotFound(t *testing.T) { handler := New(clientip.Resolver{}) req := httptest.NewRequest(http.MethodGet, "/missing", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("got status %d, want %d", rec.Code, http.StatusNotFound) } }