From de799a89c3eb2377531da666cc3fa00963b82e1f Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 26 Aug 2025 08:08:58 +0000 Subject: [PATCH 1/3] fix: panic Signed-off-by: Asish Kumar --- jsondiff.go | 70 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/jsondiff.go b/jsondiff.go index 9a62385..9e1e89e 100644 --- a/jsondiff.go +++ b/jsondiff.go @@ -204,27 +204,48 @@ func calculateJSONDiffs(expectedJSON, actualJSON []byte) (string, error) { return strings.Join(diffs, "\n"), nil } +// unquoteKey trims surrounding quotes and spaces from a JSON key safely. +func unquoteKey(k string) string { + k = strings.TrimSpace(k) + // Trim both double/single quotes if present. + k = strings.Trim(k, `"'`) + return k +} + // extractKey extracts the keys from the diff string. -// diffString: The input string representing the differences. -// Returns a string containing all the keys separated by a pipe character. +// Handles empty lines and lines that don't start with +/- safely. func extractKey(diffString string) string { - diffLines := strings.Split(diffString, "\n") // Split the diff string into lines. + diffLines := strings.Split(diffString, "\n") var keys []string - // Iterate over each line in the diff string. - for _, line := range diffLines { - // Remove the leading '-' or '+' and any surrounding spaces - line = strings.TrimSpace(line[1:]) + for _, raw := range diffLines { + line := strings.TrimSpace(raw) + if line == "" { + continue + } + + // Remove a single leading +/- if present. + if line[0] == '-' || line[0] == '+' { + // If it's exactly "-" or "+", skip. + if len(line) == 1 { + continue + } + line = strings.TrimSpace(line[1:]) + if line == "" { + continue + } + } - if colonIndex := strings.Index(line, ":"); colonIndex != -1 { - // Extract and clean up the key - key := strings.Trim(line[:colonIndex], `"'`) + // Expect `: `; split once. + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + continue + } + key := unquoteKey(parts[0]) + if key != "" { keys = append(keys, key) } - // Add the key to the list of keys. } - - // Join the keys into a single string separated by a pipe character. return strings.Join(keys, "|") } @@ -484,13 +505,14 @@ func separateAndColorize(diffStr string, noise map[string][]string) (string, str actualTrimmedLine := nextLine[3:] // Trim the '+ ' prefix from the next line. actualKeyValue := strings.SplitN(actualTrimmedLine, ":", 2) actualKey = strings.TrimSpace(actualKeyValue[0]) + cleanActualKey := unquoteKey(actualKey) // Process the value value := strings.TrimSpace(actualKeyValue[1]) var jsonObj map[string]interface{} switch { case json.Unmarshal([]byte(value), &jsonObj) == nil: isActualMap = true - actualMap = map[string]interface{}{actualKey[:len(actualKey)-1]: jsonObj} + actualMap = map[string]interface{}{cleanActualKey: jsonObj} case json.Unmarshal([]byte(value), &actualsArray) == nil: default: actualValue = value @@ -501,13 +523,15 @@ func separateAndColorize(diffStr string, noise map[string][]string) (string, str expectTrimmedLine := line[3:] // Trim the '- ' prefix from the current line. expectkeyValue := strings.SplitN(expectTrimmedLine, ":", 2) expectKey = strings.TrimSpace(expectkeyValue[0]) + cleanExpectKey := unquoteKey(expectKey) // Process the value value := strings.TrimSpace(expectkeyValue[1]) var jsonObj map[string]interface{} switch { case json.Unmarshal([]byte(value), &jsonObj) == nil: isExpectMap = true - expectMap = map[string]interface{}{expectKey[:len(expectKey)-1]: jsonObj} + expectMap = map[string]interface{}{cleanExpectKey: jsonObj} + case json.Unmarshal([]byte(value), &expectsArray) == nil: default: expectValue = value @@ -523,19 +547,23 @@ func separateAndColorize(diffStr string, noise map[string][]string) (string, str if expectValue != nil && actualValue != nil { var expectBuilder, actualBuilder strings.Builder - if expectKey != actualKey { - actualBuilder.WriteString(fmt.Sprintf("%s: %s\n", green(serialize(actualKey[:len(actualKey)-1])), actualValue)) - expectBuilder.WriteString(fmt.Sprintf("%s: %s\n", red(serialize(expectKey[:len(expectKey)-1])), expectValue)) + eKey := unquoteKey(expectKey) + aKey := unquoteKey(actualKey) + if eKey != aKey { + actualBuilder.WriteString(fmt.Sprintf("%s: %s\n", green(serialize(aKey)), actualValue)) + expectBuilder.WriteString(fmt.Sprintf("%s: %s\n", red(serialize(eKey)), expectValue)) } else { - compare(expectKey[:len(expectKey)-1], expectValue, actualValue, " ", &expectBuilder, &actualBuilder, red, green, intialJsonPath, noise) + compare(eKey, expectValue, actualValue, " ", &expectBuilder, &actualBuilder, red, green, intialJsonPath, noise) } expectedText = expectBuilder.String() actualText = actualBuilder.String() } else if !isExpectMap || !isActualMap { - if actualKey != expectKey { + eKey := unquoteKey(expectKey) + aKey := unquoteKey(actualKey) + if aKey != eKey { continue } - isNoised := checkNoise(actualKey, noise) + isNoised := checkNoise(aKey, noise) if isNoised { continue } From af33f7ef42db01bda440ee54dd7f6bbb3e49f365 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 26 Aug 2025 08:44:01 +0000 Subject: [PATCH 2/3] feat: fuzzer for robust testing Signed-off-by: Asish Kumar --- .github/workflows/test.yml | 30 +++++++++---- jsonDiff_test.go | 89 ++++++++++++++++++++++++++++++++++++++ jsondiff.go | 8 ++-- 3 files changed, 115 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a33a48..cdac005 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,14 +11,26 @@ permissions: contents: read jobs: - test: - name: Tests + unit-tests: + name: Unit Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 - with: - go-version: '1.22' - cache: false - - name: Run tests - run: go test -v ./... \ No newline at end of file + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.22' + cache: true + - name: Run unit tests + run: go test -v ./... + + fuzz-tests: + name: Fuzz Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.22' + cache: true + - name: Run fuzz tests + run: go test -fuzz=FuzzCompareJSON -fuzztime=240s ./... diff --git a/jsonDiff_test.go b/jsonDiff_test.go index ad78021..58d5757 100644 --- a/jsonDiff_test.go +++ b/jsonDiff_test.go @@ -868,3 +868,92 @@ func wrapTextWithAnsi(input string) string { // Return the processed string with properly wrapped ANSI escape sequences. return wrappedBuilder.String() } + +func TestExtractKey(t *testing.T) { + tests := []struct { + name string + inputDiff string + expectedKeys string + }{ + { + name: "Standard case with plus and minus", + inputDiff: "- \"name\": \"Cat\"\n+ \"name\": \"Dog\"", + expectedKeys: "name|name", + }, + { + name: "Keys with single and double quotes", + inputDiff: `- 'id': 123` + "\n" + `+ "id": 456`, + expectedKeys: "id|id", + }, + { + name: "Handles empty lines between diffs", + inputDiff: "- \"key1\": \"val1\"\n\n+ \"key1\": \"val2\"", + expectedKeys: "key1|key1", + }, + { + name: "Handles leading and trailing empty lines", + inputDiff: "\n- \"key1\": \"val1\"\n+ \"key1\": \"val2\"\n", + expectedKeys: "key1|key1", + }, + { + name: "Empty input string", + inputDiff: "", + expectedKeys: "", + }, + { + name: "Input with only whitespace and newlines", + inputDiff: " \n \n ", + expectedKeys: "", + }, + { + name: "Malformed line without a colon", + inputDiff: "- \"key1\" \"val1\"", + expectedKeys: "", + }, + { + name: "Line with only a plus or minus sign", + inputDiff: "-\n+", + expectedKeys: "", + }, + { + name: "Mixed valid and invalid lines", + inputDiff: "- \"validKey1\": 1\n- malformed\n+ \"validKey2\": 2", + expectedKeys: "validKey1|validKey2", + }, + { + name: "Key with extra whitespace", + inputDiff: `- " spaced key " : "value"`, + expectedKeys: "spaced key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractKey(tt.inputDiff) + if got != tt.expectedKeys { + t.Errorf("extractKey() = %v, want %v", got, tt.expectedKeys) + } + }) + } +} + +func FuzzCompareJSON(f *testing.F) { + // Add some seed inputs to give the fuzzer a starting point. + f.Add(`{"id": 1}`, `{"id": 2}`) + f.Add(`{"a": "b"}`, `{}`) + f.Add(`[]`, `{}`) + f.Add(`"a"`, `"b"`) + f.Add(`- "key": val`, `+ "key": val`) // Seed for extractKey + + // The fuzzer will now generate random strings and run them through your functions. + f.Fuzz(func(t *testing.T, json1 string, json2 string) { + // We only care if these functions panic. We don't need to check the output. + // A panic will automatically fail the test. + + // Fuzz the main function + _, _ = CompareJSON([]byte(json1), []byte(json2), nil, false) + + // Fuzz the function that was originally panicking + _ = extractKey(json1) + }) +} diff --git a/jsondiff.go b/jsondiff.go index 9e1e89e..d4b7ddc 100644 --- a/jsondiff.go +++ b/jsondiff.go @@ -207,8 +207,10 @@ func calculateJSONDiffs(expectedJSON, actualJSON []byte) (string, error) { // unquoteKey trims surrounding quotes and spaces from a JSON key safely. func unquoteKey(k string) string { k = strings.TrimSpace(k) - // Trim both double/single quotes if present. + // Trim both double/single quotes if present first. k = strings.Trim(k, `"'`) + // Then, trim any remaining whitespace that was inside the quotes. + k = strings.TrimSpace(k) return k } @@ -519,7 +521,7 @@ func separateAndColorize(diffStr string, noise map[string][]string) (string, str } } - if len(strings.SplitN(line[3:], ":", 2)) == 2 { + if len(line) > 3 && len(strings.SplitN(line[3:], ":", 2)) == 2 { expectTrimmedLine := line[3:] // Trim the '- ' prefix from the current line. expectkeyValue := strings.SplitN(expectTrimmedLine, ":", 2) expectKey = strings.TrimSpace(expectkeyValue[0]) @@ -778,7 +780,7 @@ func insertEmptyLines(lines []string) []string { result = append(result, lines[i]) // Append the current line to the result slice. // Check if the current line and the next line start with the same symbol. - if i < len(lines)-1 && lines[i] != "" && lines[i][0] == lines[i+1][0] { + if i < len(lines)-1 && lines[i] != "" && lines[i+1] != "" && lines[i][0] == lines[i+1][0] { result = append(result, "") // Insert an empty line between consecutive elements with the same symbol. } } From d6e36ffe40cd6c234850f396a1d3003de7acc2e6 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 26 Aug 2025 10:14:39 +0000 Subject: [PATCH 3/3] feat: add fuzz testdata folder to gitignore Signed-off-by: Asish Kumar --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6f6f5e6..439a3e3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ # Dependency directories (remove the comment below to include it) # vendor/ +# Fuzz test data +testdata + # Go workspace file go.work go.work.sum