package handlers_test import ( "bytes" "encoding/json" "errors" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "sneak.berlin/go/netwatch/internal/handlers" ) var errStorageFailed = errors.New("storage failed") // stubAppender drives the storage success/failure path without a // real buffer or disk. type stubAppender struct { err error } func (s stubAppender) Append(any) error { return s.err } func newTestHandlers(buf stubAppender, out io.Writer) *handlers.Handlers { return handlers.NewForTest(buf, slog.New(slog.NewJSONHandler(out, nil))) } func decodeStatus(t *testing.T, body []byte) string { t.Helper() var resp struct { Status string `json:"status"` } err := json.Unmarshal(body, &resp) if err != nil { t.Fatalf("response body not JSON: %v (%q)", err, body) } return resp.Status } func TestHandleReportStorageFailureIsNon2xx(t *testing.T) { t.Parallel() h := newTestHandlers(stubAppender{err: errStorageFailed}, io.Discard) rec := httptest.NewRecorder() req := httptest.NewRequest( http.MethodPost, "/api/v1/reports", strings.NewReader(`{"clientId":"c1","hosts":[]}`), ) h.HandleReport().ServeHTTP(rec, req) if rec.Code < 500 { t.Fatalf("storage failure status = %d, want a 5xx", rec.Code) } if got := decodeStatus(t, rec.Body.Bytes()); got != "error" { t.Fatalf("status field = %q, want %q", got, "error") } } func TestHandleReportMalformedJSONIs400(t *testing.T) { t.Parallel() h := newTestHandlers(stubAppender{}, io.Discard) rec := httptest.NewRecorder() req := httptest.NewRequest( http.MethodPost, "/api/v1/reports", strings.NewReader(`{not json`), ) h.HandleReport().ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("malformed status = %d, want %d", rec.Code, http.StatusBadRequest) } } func TestHandleReportOversizeIs413(t *testing.T) { t.Parallel() const limit = 32 h := newTestHandlers(stubAppender{}, io.Discard) rec := httptest.NewRecorder() req := httptest.NewRequest( http.MethodPost, "/api/v1/reports", strings.NewReader(`{"clientId":"`+strings.Repeat("x", 200)+`"}`), ) // Emulate the body-size middleware capping the body so the // handler observes a *http.MaxBytesError while decoding. req.Body = http.MaxBytesReader(rec, req.Body, limit) h.HandleReport().ServeHTTP(rec, req) if rec.Code != http.StatusRequestEntityTooLarge { t.Fatalf("oversize status = %d, want %d", rec.Code, http.StatusRequestEntityTooLarge) } } func TestHandleReportDoesNotLogRawGeo(t *testing.T) { t.Parallel() const sentinel = "SENSITIVE-GEO-BLOB" var logbuf bytes.Buffer h := newTestHandlers(stubAppender{}, &logbuf) rec := httptest.NewRecorder() req := httptest.NewRequest( http.MethodPost, "/api/v1/reports", strings.NewReader( `{"clientId":"c1","geo":{"raw":"`+sentinel+`"},"hosts":[]}`, ), ) h.HandleReport().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } if strings.Contains(logbuf.String(), sentinel) { t.Fatal("raw geo bytes were written to the log") } if !strings.Contains(logbuf.String(), "geo_bytes") { t.Fatal("expected a bounded geo_bytes field in the log") } }