From 5a38283bb093011371e22d2450495f45d75d7dd6 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 8 Jul 2025 16:04:28 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Add=20JSONBodyHandler=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented JSONBodyHandler for automatic JSON body parsing. - Added comprehensive unit tests for JSONBodyHandler covering successful decoding, invalid JSON, empty bodies, and error propagation. - Refactored handler_test.go to improve test structure and resolve linting issues (funlen, gocritic). --- handler.go | 20 ++++++++++ handler_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/handler.go b/handler.go index f9b8d66..63c20c5 100644 --- a/handler.go +++ b/handler.go @@ -1,6 +1,7 @@ package ezhandler import ( + "encoding/json" "io" "net/http" ) @@ -42,3 +43,22 @@ func (fn ResponseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) erro _, err = io.Copy(w, body) return err } + +// JSONBodyHandler can be used as a [Handler] which automatically parses the json body into a value, which is passed to the function. +type JSONBodyHandler[V any] func(w http.ResponseWriter, r *http.Request, body V) error + +var _ Handler = JSONBodyHandler[map[string]any](nil) + +func (fn JSONBodyHandler[V]) ServeHTTP(w http.ResponseWriter, r *http.Request) error { + reqBody := r.Body + //nolint:errcheck // This is usually fine + defer reqBody.Close() + + dec := json.NewDecoder(reqBody) + var body V + if err := dec.Decode(&body); err != nil { + return err + } + + return fn(w, r, body) +} diff --git a/handler_test.go b/handler_test.go index 5cd5c25..66c3aca 100644 --- a/handler_test.go +++ b/handler_test.go @@ -1,8 +1,10 @@ package ezhandler_test import ( + "io" "net/http" "net/http/httptest" + "strings" "testing" "codeberg.org/danjones000/ezhandler" @@ -46,3 +48,99 @@ func TestHandlerFunc_ServeHTTP(t *testing.T) { }) } } + +type testBody struct { + Name string `json:"name"` + Age int `json:"age"` +} + +type jsonBodyTest struct { + name string + requestBody string + handler ezhandler.JSONBodyHandler[testBody] + expectedStatus int + expectedBody string + expectedErr error + expectedErrContains string + additionAssert func(*assert.Assertions, testBody) +} + +var jsonBodyHandlerTests = []jsonBodyTest{ + { + name: "successful decoding and handler execution", + requestBody: `{"name":"John Doe","age":30}`, + handler: ezhandler.JSONBodyHandler[testBody](func(w http.ResponseWriter, r *http.Request, body testBody) error { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil + }), + expectedStatus: http.StatusOK, + expectedBody: "success", + expectedErr: nil, + additionAssert: func(as *assert.Assertions, body testBody) { + as.Equal("John Doe", body.Name) + as.Equal(30, body.Age) + }, + }, + { + name: "invalid JSON body", + requestBody: `{"name":"John Doe","age":}`, + handler: ezhandler.JSONBodyHandler[testBody](func(w http.ResponseWriter, r *http.Request, body testBody) error { + return nil // Should not be called + }), + expectedErrContains: "invalid character", + }, + { + name: "empty body for struct type", + requestBody: "", + handler: ezhandler.JSONBodyHandler[testBody](func(w http.ResponseWriter, r *http.Request, body testBody) error { + return nil // Should not be called + }), + expectedErr: io.EOF, + }, + { + name: "handler function returns error", + requestBody: `{"name":"Jane Doe","age":25}`, + handler: ezhandler.JSONBodyHandler[testBody](func(w http.ResponseWriter, r *http.Request, body testBody) error { + return errTest + }), + expectedErr: errTest, + }, +} + +func TestJSONBodyHandler_ServeHTTP(t *testing.T) { + for _, tc := range jsonBodyHandlerTests { + runJSONBodyHandlerTest(t, tc) + } +} + +func runJSONBodyHandlerTest(tt *testing.T, tc jsonBodyTest) { + tt.Run(tc.name, func(t *testing.T) { + handler := ezhandler.JSONBodyHandler[testBody](func(w http.ResponseWriter, r *http.Request, body testBody) error { + if err := tc.handler(w, r, body); err != nil { + return err + } + if tc.additionAssert != nil { + as := assert.New(t) + tc.additionAssert(as, body) + } + return nil + }) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tc.requestBody)) + rec := httptest.NewRecorder() + + err := handler.ServeHTTP(rec, req) + + switch { + case tc.expectedErr != nil: + assert.Equal(t, tc.expectedErr, err) + case tc.expectedErrContains != "": + assert.ErrorContains(t, err, tc.expectedErrContains) + default: + assert.NoError(t, err) + assert.Equal(t, tc.expectedStatus, rec.Code) + assert.Equal(t, tc.expectedBody, rec.Body.String()) + } + + }) +}