Skip to content

Commit 5a1a2d9

Browse files
committed
refactor(tests): improve fuzz testing for P2P message unmarshaling
Refactored fuzz tests to enhance clarity and maintainability. Added helper functions for seed corpus and validation of message round-trip consistency.
1 parent e212d27 commit 5a1a2d9

File tree

1 file changed

+59
-76
lines changed

1 file changed

+59
-76
lines changed

client_test.go

Lines changed: 59 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,8 @@ func (e *mockError) Error() string {
601601

602602
func TestBuildHostOptionsWithAnnounceAddrs(t *testing.T) {
603603
logger := &DefaultLogger{}
604+
// noop cancel function for testing - buildHostOptions requires a cancel function
605+
// but we don't need actual cancellation logic in this unit test
604606
cancel := func() {}
605607

606608
tests := []struct {
@@ -725,133 +727,114 @@ func TestClientPeerCacheTTLCustom(t *testing.T) {
725727
require.NoError(t, err)
726728
}
727729

728-
// FuzzMessageUnmarshal performs fuzz testing on the P2P message unmarshaling logic
729-
// to ensure it handles arbitrary JSON data without panicking. This tests the
730-
// robustness of message parsing from potentially malicious or corrupted peers.
731-
func FuzzMessageUnmarshal(f *testing.F) {
732-
// Seed corpus with various P2P message formats
733-
734-
// 1. Valid message
735-
f.Add([]byte(`{"name":"alice","data":"aGVsbG8="}`)) // "hello" in base64
730+
// messageStruct represents the P2P message structure for testing
731+
type messageStruct struct {
732+
Name string `json:"name"`
733+
Data []byte `json:"data"`
734+
}
736735

737-
// 2. Valid message with empty data
736+
// addMessageUnmarshalSeeds adds a comprehensive seed corpus for P2P message fuzz testing
737+
func addMessageUnmarshalSeeds(f *testing.F) {
738+
// Valid messages
739+
f.Add([]byte(`{"name":"alice","data":"aGVsbG8="}`))
738740
f.Add([]byte(`{"name":"bob","data":""}`))
739-
740-
// 3. Valid message with null data
741741
f.Add([]byte(`{"name":"charlie","data":null}`))
742742

743-
// 4. Missing name field
743+
// Missing fields
744744
f.Add([]byte(`{"data":"aGVsbG8="}`))
745-
746-
// 5. Missing data field
747745
f.Add([]byte(`{"name":"alice"}`))
748-
749-
// 6. Empty JSON object
750746
f.Add([]byte(`{}`))
751747

752-
// 7. Invalid JSON
748+
// Invalid JSON
753749
f.Add([]byte(`{invalid json`))
754750
f.Add([]byte(`}`))
755751
f.Add([]byte(`{"name":"alice","data":`))
756752

757-
// 8. Wrong data types
753+
// Wrong data types
758754
f.Add([]byte(`{"name":123,"data":"test"}`))
759755
f.Add([]byte(`{"name":"alice","data":123}`))
760756
f.Add([]byte(`{"name":"alice","data":{"nested":"object"}}`))
761757
f.Add([]byte(`{"name":"alice","data":["array","of","items"]}`))
762758

763-
// 9. Not an object
759+
// Not an object
764760
f.Add([]byte(`[]`))
765761
f.Add([]byte(`"string"`))
766762
f.Add([]byte(`123`))
767763
f.Add([]byte(`null`))
768764
f.Add([]byte(`true`))
769765

770-
// 10. Extra fields
766+
// Extra fields
771767
f.Add([]byte(`{"name":"alice","data":"test","extra":"field","another":123}`))
772768

773-
// 11. Very long strings
769+
// Very long strings
774770
longName := `{"name":"` + string(make([]byte, 1000)) + `","data":"test"}`
775771
f.Add([]byte(longName))
776772

777-
// 12. Unicode and special characters (using escape sequences to avoid gosmopolitan warning)
778-
f.Add([]byte(`{"name":"peer\u4f60\u597d\u4e16\u754c","data":"test"}`)) // Chinese characters
773+
// Unicode and special characters
774+
f.Add([]byte(`{"name":"peer\u4f60\u597d\u4e16\u754c","data":"test"}`))
779775
f.Add([]byte(`{"name":"alice\u0000null","data":"test"}`))
780776
f.Add([]byte(`{"name":"alice","data":"data\u0000null"}`))
781777

782-
// 13. Escape sequences
778+
// Escape sequences
783779
f.Add([]byte(`{"name":"alice\"bob","data":"test"}`))
784780
f.Add([]byte(`{"name":"alice\\bob","data":"test"}`))
785781

786-
// 14. Empty string
782+
// Edge cases
787783
f.Add([]byte(``))
788-
789-
// 15. Nested JSON in data field (as string)
790784
f.Add([]byte(`{"name":"alice","data":"{\"nested\":\"json\"}"}`))
791-
792-
// 16. Binary data encoded as base64 (proper use case)
793785
f.Add([]byte(`{"name":"alice","data":"SGVsbG8gV29ybGQhIFRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2Uu"}`))
786+
}
794787

795-
f.Fuzz(func(t *testing.T, jsonData []byte) {
796-
// Simulate the message unmarshaling logic from client.go:616-623
797-
var m struct {
798-
Name string `json:"name"`
799-
Data []byte `json:"data"`
800-
}
788+
// validateMessageRoundTrip verifies that message data is preserved through marshal/unmarshal cycle
789+
func validateMessageRoundTrip(t *testing.T, m, m2 messageStruct) {
790+
if m.Name != m2.Name {
791+
t.Errorf("Name changed after round-trip: %q -> %q", m.Name, m2.Name)
792+
}
801793

802-
// The function should never panic, regardless of input
803-
err := json.Unmarshal(jsonData, &m)
804-
// Verify behavior is consistent
805-
if err != nil {
806-
// Error is expected for invalid JSON - the client code logs this error and continues
807-
// We just verified no panic occurred, which is the main goal
808-
return
794+
if len(m.Data) != len(m2.Data) {
795+
t.Errorf("Data length changed after round-trip: %d -> %d", len(m.Data), len(m2.Data))
796+
return
797+
}
798+
799+
for i := range m.Data {
800+
if m.Data[i] != m2.Data[i] {
801+
t.Errorf("Data byte at index %d changed after round-trip: %d -> %d", i, m.Data[i], m2.Data[i])
802+
break
809803
}
804+
}
805+
}
806+
807+
// FuzzMessageUnmarshal performs fuzz testing on the P2P message unmarshaling logic
808+
// to ensure it handles arbitrary JSON data without panicking. This tests the
809+
// robustness of message parsing from potentially malicious or corrupted peers.
810+
func FuzzMessageUnmarshal(f *testing.F) {
811+
addMessageUnmarshalSeeds(f)
810812

811-
// Name should be a string (could be empty)
812-
_ = m.Name
813+
f.Fuzz(func(t *testing.T, jsonData []byte) {
814+
var m messageStruct
813815

814-
// Data should be a byte slice (could be nil or empty)
815-
if m.Data != nil {
816-
// Data exists, verify it's a valid byte slice
817-
_ = len(m.Data)
816+
err := json.Unmarshal(jsonData, &m)
817+
if err != nil {
818+
return // Invalid JSON is expected and handled gracefully
818819
}
819820

820-
// Test that we can re-marshal the data
821-
remarshaled, marshalErr := json.Marshal(m)
822-
if marshalErr != nil {
823-
t.Errorf("Failed to re-marshal successfully unmarshaled message: %v", marshalErr)
821+
remarshaled, err := json.Marshal(m)
822+
if err != nil {
823+
t.Errorf("Failed to re-marshal successfully unmarshaled message: %v", err)
824+
return
824825
}
825826

826-
// Re-marshaled data should be valid JSON
827827
if len(remarshaled) == 0 {
828828
t.Error("Re-marshaled data is empty")
829+
return
829830
}
830831

831-
// Test that we can unmarshal the re-marshaled data
832-
var m2 struct {
833-
Name string `json:"name"`
834-
Data []byte `json:"data"`
835-
}
836-
if unmarshalErr := json.Unmarshal(remarshaled, &m2); unmarshalErr != nil {
837-
t.Errorf("Failed to unmarshal re-marshaled data: %v", unmarshalErr)
838-
}
839-
840-
// Round-trip should preserve the data
841-
if m.Name != m2.Name {
842-
t.Errorf("Name changed after round-trip: %q -> %q", m.Name, m2.Name)
832+
var m2 messageStruct
833+
if err := json.Unmarshal(remarshaled, &m2); err != nil {
834+
t.Errorf("Failed to unmarshal re-marshaled data: %v", err)
835+
return
843836
}
844837

845-
// For data, compare lengths and content
846-
if len(m.Data) != len(m2.Data) {
847-
t.Errorf("Data length changed after round-trip: %d -> %d", len(m.Data), len(m2.Data))
848-
} else {
849-
for i := range m.Data {
850-
if m.Data[i] != m2.Data[i] {
851-
t.Errorf("Data byte at index %d changed after round-trip: %d -> %d", i, m.Data[i], m2.Data[i])
852-
break
853-
}
854-
}
855-
}
838+
validateMessageRoundTrip(t, m, m2)
856839
})
857840
}

0 commit comments

Comments
 (0)