Skip to content

Commit 5568523

Browse files
vishrclaude
andcommitted
test(json): cover pooled-buffer Deserialize behaviors
Add correctness coverage for the new pooled-buffer JSON deserializer, addressing gaps surfaced in review: - RejectsTrailingData: documents that json.Unmarshal rejects content after the first top-level value (a behavior change from streaming json.Decoder). - PooledBufferReuse: long body followed by a short one must not leak stale bytes through the reused buffer. - PooledBufferConcurrent: many goroutines decoding distinct bodies; under -race this catches any aliasing/missing-reset regression (data bleed). - LargeBodyThenNormal: exercises the >64 KiB buffer-cap path and that the oversized buffer does not corrupt the next request. - BodyReadError: a failing body read surfaces as 400, matching prior behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 99c411e commit 5568523

1 file changed

Lines changed: 117 additions & 1 deletion

File tree

json_test.go

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@
44
package echo
55

66
import (
7-
"github.com/stretchr/testify/assert"
7+
"errors"
8+
"fmt"
89
"net/http"
910
"net/http/httptest"
1011
"strings"
12+
"sync"
1113
"testing"
14+
15+
"github.com/stretchr/testify/assert"
1216
)
1317

18+
// deserializeJSON decodes body into target via the default serializer using a
19+
// fresh context. It does not touch *testing.T so it is safe to call from
20+
// goroutines (used by the concurrency test).
21+
func deserializeJSON(e *Echo, body string, target any) error {
22+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
23+
c := e.NewContext(req, httptest.NewRecorder())
24+
return DefaultJSONSerializer{}.Deserialize(c, target)
25+
}
26+
1427
// Note this test is deliberately simple as there's not a lot to test.
1528
// Just need to ensure it writes JSONs. The heavy work is done by the context methods.
1629
func TestDefaultJSONCodec_Encode(t *testing.T) {
@@ -98,3 +111,106 @@ func TestDefaultJSONCodec_Decode(t *testing.T) {
98111
assert.EqualError(t, err, "code=400, message=Bad Request, err=json: cannot unmarshal number into Go struct field .id of type string")
99112

100113
}
114+
115+
// TestDefaultJSONCodec_Decode_RejectsTrailingData documents an intentional
116+
// behavior change: Deserialize uses json.Unmarshal, which (unlike a streaming
117+
// json.Decoder) rejects any content after the first top-level JSON value.
118+
func TestDefaultJSONCodec_Decode_RejectsTrailingData(t *testing.T) {
119+
e := New()
120+
for _, body := range []string{
121+
userJSON + `{"id":2,"name":"second"}`, // a second JSON object
122+
userJSON + ` trailing garbage`, // trailing non-JSON
123+
userJSON + `,`, // trailing token
124+
} {
125+
var u user
126+
err := deserializeJSON(e, body, &u)
127+
if assert.Error(t, err, "body %q should be rejected", body) {
128+
assert.IsType(t, &HTTPError{}, err)
129+
assert.Equal(t, http.StatusBadRequest, err.(*HTTPError).Code)
130+
}
131+
}
132+
}
133+
134+
// TestDefaultJSONCodec_Decode_PooledBufferReuse guards against stale bytes
135+
// bleeding between requests through the reused pooled buffer: a long body
136+
// followed by a short one must each decode to exactly their own input.
137+
func TestDefaultJSONCodec_Decode_PooledBufferReuse(t *testing.T) {
138+
e := New()
139+
for i := 0; i < 50; i++ {
140+
longName := strings.Repeat("x", 1000+i)
141+
var long user
142+
err := deserializeJSON(e, fmt.Sprintf(`{"id":%d,"name":%q}`, i, longName), &long)
143+
assert.NoError(t, err)
144+
assert.Equal(t, user{ID: i, Name: longName}, long)
145+
146+
var short user
147+
err = deserializeJSON(e, `{"id":7,"name":"a"}`, &short)
148+
assert.NoError(t, err)
149+
assert.Equal(t, user{ID: 7, Name: "a"}, short)
150+
}
151+
}
152+
153+
// TestDefaultJSONCodec_Decode_PooledBufferConcurrent exercises the pooled
154+
// buffer from many goroutines at once; run under -race it catches any aliasing
155+
// or missing-reset regression that would let one request's body corrupt another.
156+
func TestDefaultJSONCodec_Decode_PooledBufferConcurrent(t *testing.T) {
157+
e := New()
158+
const n = 64
159+
var wg sync.WaitGroup
160+
errs := make([]error, n)
161+
got := make([]user, n)
162+
for i := 0; i < n; i++ {
163+
wg.Add(1)
164+
go func(i int) {
165+
defer wg.Done()
166+
body := fmt.Sprintf(`{"id":%d,"name":%q}`, i, strings.Repeat("n", i+1))
167+
errs[i] = deserializeJSON(e, body, &got[i])
168+
}(i)
169+
}
170+
wg.Wait()
171+
for i := 0; i < n; i++ {
172+
assert.NoError(t, errs[i])
173+
assert.Equal(t, user{ID: i, Name: strings.Repeat("n", i+1)}, got[i])
174+
}
175+
}
176+
177+
// TestDefaultJSONCodec_Decode_LargeBodyThenNormal covers the buffer-cap path: a
178+
// body larger than maxPooledJSONBuf must decode correctly, and its oversized
179+
// buffer (dropped from the pool rather than retained) must not affect the next
180+
// normal-sized request.
181+
func TestDefaultJSONCodec_Decode_LargeBodyThenNormal(t *testing.T) {
182+
e := New()
183+
bigName := strings.Repeat("z", 100*1024) // 100 KiB > 64 KiB cap
184+
var big user
185+
err := deserializeJSON(e, fmt.Sprintf(`{"id":1,"name":%q}`, bigName), &big)
186+
assert.NoError(t, err)
187+
assert.Equal(t, user{ID: 1, Name: bigName}, big)
188+
189+
var small user
190+
err = deserializeJSON(e, userJSON, &small)
191+
assert.NoError(t, err)
192+
assert.Equal(t, user{ID: 1, Name: "Jon Snow"}, small)
193+
}
194+
195+
// errReader is an io.ReadCloser whose Read always fails, used to exercise the
196+
// body-read error branch of Deserialize.
197+
type errReader struct{}
198+
199+
func (errReader) Read([]byte) (int, error) { return 0, errors.New("read failed") }
200+
func (errReader) Close() error { return nil }
201+
202+
// TestDefaultJSONCodec_Decode_BodyReadError verifies a failing request body read
203+
// surfaces as a 400, matching the pre-existing decoder behavior.
204+
func TestDefaultJSONCodec_Decode_BodyReadError(t *testing.T) {
205+
e := New()
206+
req := httptest.NewRequest(http.MethodPost, "/", http.NoBody)
207+
req.Body = errReader{}
208+
c := e.NewContext(req, httptest.NewRecorder())
209+
210+
var u user
211+
err := DefaultJSONSerializer{}.Deserialize(c, &u)
212+
if assert.Error(t, err) {
213+
assert.IsType(t, &HTTPError{}, err)
214+
assert.Equal(t, http.StatusBadRequest, err.(*HTTPError).Code)
215+
}
216+
}

0 commit comments

Comments
 (0)