package handlers_test import ( "bytes" "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "net/netip" "strings" "testing" "github.com/go-chi/chi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/config" "sneak.berlin/go/webhooker/internal/database" "sneak.berlin/go/webhooker/internal/handlers" "sneak.berlin/go/webhooker/internal/middleware" ) // TestHandleWebhook_LogsClientNextToThePeer checks that the // receiver's "webhook request received" line carries both addresses: // remoteIP, the connecting peer, and clientIP, the client the access // log attributes the request to. func TestHandleWebhook_LogsClientNextToThePeer(t *testing.T) { t.Parallel() // untrustedPeer is outside the trusted 10.0.0.0/8, so its // X-Forwarded-For is ignored and it is the client. const untrustedPeer = "192.0.2.10" cases := map[string]struct { peer string wantRemote string wantClient string }{ "trusted proxy with a forwarded chain": { peer: "10.0.0.1:44444", wantRemote: "10.0.0.1", wantClient: "198.51.100.7", }, "untrusted peer": { peer: untrustedPeer + ":5555", wantRemote: untrustedPeer, wantClient: untrustedPeer, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { t.Parallel() var ( h *handlers.Handlers mw *middleware.Middleware db *database.Database ) app := newTestAppWithConfig(t, &config.Config{ DataDir: t.TempDir(), TrustedProxies: []netip.Prefix{ netip.MustParsePrefix("10.0.0.0/8"), }, }, &h, &mw, &db) app.RequireStart() t.Cleanup(app.RequireStop) buf := new(bytes.Buffer) h.SetLogForTest(slog.New(slog.NewJSONHandler(buf, nil))) webhook := seedWebhook(t, db) seedEntrypoint(t, db, webhook.ID) // Logging is what works the client address out, so the // request goes through it as it does in production. router := chi.NewRouter() router.Use(mw.Logging()) router.Post("/h/{uuid}", h.HandleWebhook()) req := httptest.NewRequestWithContext( context.Background(), http.MethodPost, "/h/ep-"+webhook.ID, strings.NewReader("{}"), ) req.RemoteAddr = tc.peer req.Header.Set("X-Forwarded-For", "198.51.100.7, 10.0.0.2") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) line := receivedLine(t, buf) assert.Equal(t, tc.wantRemote, line["remoteIP"]) assert.Equal(t, tc.wantClient, line["clientIP"]) }) } } // receivedLine returns the one "webhook request received" line in the // captured JSON log. func receivedLine(t *testing.T, buf *bytes.Buffer) map[string]any { t.Helper() var found []map[string]any for line := range strings.SplitSeq( strings.TrimSpace(buf.String()), "\n", ) { var entry map[string]any require.NoError(t, json.Unmarshal([]byte(line), &entry)) if entry["msg"] == "webhook request received" { found = append(found, entry) } } require.Len(t, found, 1) return found[0] }