// Copyright 2025 The Go MCP SDK Authors. All rights reserved. // Use of this source code is governed by the license // that can be found in the LICENSE file. package json import ( "bytes" "encoding/json" "errors" "fmt" "strings" "testing" "time" "github.com/google/go-cmp/cmp" ) func TestUnmarshalCaseSensitivity(t *testing.T) { type Nested struct { Field string `json:"field"` } type Target struct { Field string TaggedField string `json:"custom_tag"` Nested *Nested } tests := []struct { name string json string want Target }{ { name: "exact match", json: `{"Field": "value", "custom_tag": "tagged", "Nested": {"field": "nested"}}`, want: Target{ Field: "value", TaggedField: "tagged", Nested: &Nested{ Field: "nested", }, }, }, { name: "case mismatch", json: `{"field": "value", "Custom_tag": "tagged", "Nested": {"Field": "nested"}}`, want: Target{ Field: "", TaggedField: "", Nested: &Nested{ Field: "", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var got Target if err := Unmarshal([]byte(tt.json), &got); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("Unmarshal mismatch (-want +got):\n%s", diff) } }) } } func TestNewDecoderCaseSensitivity(t *testing.T) { type Target struct { Field string `json:"field"` TaggedField string `json:"custom_tag"` } tests := []struct { name string json string want Target }{ { name: "exact match", json: `{"field": "value", "custom_tag": "tagged"}`, want: Target{Field: "value", TaggedField: "tagged"}, }, { name: "case mismatch", json: `{"Field": "value", "Custom_tag": "tagged"}`, want: Target{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var got Target dec := NewDecoder(strings.NewReader(tt.json)) if err := dec.Decode(&got); err != nil { t.Fatalf("Decode failed: %v", err) } if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("Decode mismatch (-want +got):\n%s", diff) } }) } } func TestUnmarshalNullCharacter(t *testing.T) { type Target struct { Field string `json:"field"` } tests := []struct { name string json string want string }{ { name: "null char in middle", json: `{"fi\u0000eld": "value"}`, }, { name: "null char at end", json: `{"field\u0000": "value"}`, }, { name: "null char at start", json: `{"\u0000field": "value"}`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var got Target if err := Unmarshal([]byte(tt.json), &got); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if got.Field != "" { t.Errorf("Field was set: %s", tt.json) } }) } } func TestUnmarshalChecksNestingDepth(t *testing.T) { createNesting := func(depth int) []byte { return []byte(strings.Repeat("[", depth) + strings.Repeat("]", depth)) } tests := []struct { name string data []byte wantErr bool }{ {"flat object", []byte(`{"a":1}`), false}, {"at limit", createNesting(defaultMaxDepth), false}, {"one over limit", createNesting(defaultMaxDepth + 1), true}, {"brackets in strings do not count", []byte(`{"a":"[[[[[[[[[["}`), false}, {"escaped quote in string", []byte(`{"a":"\"[[[[[["}`), false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var v any err := Unmarshal(tt.data, &v) gotDepthErr := errors.Is(err, errMaxDepthExceeded) if gotDepthErr != tt.wantErr { t.Fatalf("Unmarshal() error = %v (err=%v), want depth error = %v", gotDepthErr, err, tt.wantErr) } }) } } func TestUnmarshalMaxDepthExitsFast(t *testing.T) { data := []byte(strings.Repeat("[", 500_000) + strings.Repeat("]", 500_000)) done := make(chan error, 1) go func() { var v any done <- Unmarshal(data, &v) }() select { case err := <-done: if !errors.Is(err, errMaxDepthExceeded) { t.Fatalf("Unmarshal error = %v, want %v", err, errMaxDepthExceeded) } case <-time.After(500 * time.Millisecond): t.Fatal("Unmarshal did not reject deeply nested payload promptly") } } func FuzzUnmarshalKeyComparison(f *testing.F) { // Add seed corpus for expected valid key and the known problematic cases. f.Add("field") f.Add("FIELD") f.Add("field\x00") f.Fuzz(func(t *testing.T, key string) { // Generate valid JSON with the fuzzed key. // json.Encoder ensures the key is properly escaped (e.g. \x00 becomes \u0000). var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) if err := enc.Encode(key); err != nil { return } encodedKey := buf.Bytes() jsonData := fmt.Sprintf(`{%s: "value"}`, encodedKey) type Target struct { Field string `json:"field"` } var got Target if err := Unmarshal([]byte(jsonData), &got); err != nil { return } if got.Field == "value" && key != "field" { t.Errorf("Unmarshal matched key %q (JSON: %s) to field 'field'", key, encodedKey) } }) } func FuzzUnmarshalFieldIsolation(f *testing.F) { // Add seed corpus. f.Add("other", "value") f.Add("safe", "overwrite") f.Add("safe\x00", "attack") f.Fuzz(func(t *testing.T, key, val string) { // Generate valid JSON with the fuzzed key and value. var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) if err := enc.Encode(key); err != nil { return } encodedKey := buf.Bytes() buf.Reset() if err := enc.Encode(val); err != nil { return } encodedVal := buf.Bytes() jsonData := fmt.Sprintf(`{"safe": "value", %s: %s}`, encodedKey, encodedVal) type Target struct { Safe string `json:"safe"` } var got Target if err := Unmarshal([]byte(jsonData), &got); err != nil { return } if got.Safe != "value" && key != "safe" { t.Errorf("Field 'safe' improperly modified by key %q (JSON: %s). Got %q, want %q", key, encodedKey, got.Safe, "value") } }) }