package testutility import ( "bytes" "encoding/json" "fmt" "maps" "slices" "testing" "github.com/google/go-cmp/cmp" "github.com/tidwall/gjson" ) func Test_replaceJSONInput(t *testing.T) { t.Parallel() matcher := func(_ gjson.Result) any { return "" } tests := []struct { input string outputs map[string]string }{ { input: `{}`, outputs: map[string]string{"": `{}`, "arr.#": `{}`}, }, { input: `{ "foo": "bar" }`, outputs: map[string]string{ "does.not.exist": `{ "foo": "bar" }`, "foo.is.string": `{ "foo": "bar" }`, "foo.#": `{ "foo": "bar" }`, "#": `{ "foo": "bar" }`, "foo": `{ "foo": "" }`, }, }, { input: `{ "foo": { "inner": "bar" } }`, outputs: map[string]string{ "foo.inner": `{ "foo": { "inner": "" } }`, }, }, { input: `{ "arr": [1, 2, 3] }`, outputs: map[string]string{ "arr": `{ "arr": "" }`, "arr.1": `{ "arr": [1, "", 3] }`, "arr.#": `{ "arr": ["", "", ""] }`, "arr.#(>2)": `{ "arr": [1, 2, ""] }`, "arr.#(>1)": `{ "arr": [1, "", 3] }`, "arr.#(>1)#": `{ "arr": [1, "", ""] }`, }, }, { input: `{ "arr": [{"v": 1}, {"v": 2}, {"v": 3}] }`, outputs: map[string]string{ "arr": `{ "arr": "" }`, "arr.0.v": `{ "arr": [{"v": ""}, {"v": 2}, {"v": 3}] }`, "arr.1.v": `{ "arr": [{"v": 1}, {"v": ""}, {"v": 3}] }`, "arr.#": `{ "arr": ["", "", ""] }`, "arr.#.v": `{ "arr": [{"v": ""}, {"v": ""}, {"v": ""}] }`, "arr.#.v.#": `{ "arr": [{"v": 1}, {"v": 2}, {"v": 3}] }`, "arr.#.#.#": `{ "arr": [{"v": 1}, {"v": 2}, {"v": 3}] }`, "arr.#.#": `{ "arr": [{"v": 1}, {"v": 2}, {"v": 3}] }`, }, }, { input: `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, { "v": [{"v": 3}, {"v": 4}] }, { "v": [{"v": 5}, {"v": 6}] } ] }`, outputs: map[string]string{ "arr": `{ "arr": "" }`, "arr.0.v.0.v": `{ "arr": [ { "v": [{"v": ""}, {"v": 2}] }, { "v": [{"v": 3}, {"v": 4}] }, { "v": [{"v": 5}, {"v": 6}] } ] }`, "arr.1.v.1.v": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, { "v": [{"v": 3}, {"v": ""}] }, { "v": [{"v": 5}, {"v": 6}] } ] }`, "arr.1.v.2.v": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, { "v": [{"v": 3}, {"v": 4}] }, { "v": [{"v": 5}, {"v": 6}] } ] }`, "arr.#": `{ "arr": [ "", "", "" ] }`, "arr.1.v.#": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, { "v": ["", ""] }, { "v": [{"v": 5}, {"v": 6}] } ] }`, "arr.#.v.#": `{ "arr": [ { "v": ["", ""] }, { "v": ["", ""] }, { "v": ["", ""] } ] }`, "arr.#.v.0.v": `{ "arr": [ { "v": [{"v": ""}, {"v": 2}] }, { "v": [{"v": ""}, {"v": 4}] }, { "v": [{"v": ""}, {"v": 6}] } ] }`, "arr.#.v.1.v": `{ "arr": [ { "v": [{"v": 1}, {"v": ""}] }, { "v": [{"v": 3}, {"v": ""}] }, { "v": [{"v": 5}, {"v": ""}] } ] }`, "arr.#.v.#.v": `{ "arr": [ { "v": [{"v": ""}, {"v": ""}] }, { "v": [{"v": ""}, {"v": ""}] }, { "v": [{"v": ""}, {"v": ""}] } ] }`, "arr.1.v.#.v": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, { "v": [{"v": ""}, {"v": ""}] }, { "v": [{"v": 5}, {"v": 6}] } ] }`, }, }, { input: `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, { "v": [{"v": 3}, {"v": 4}] }, { "v": [{"v": 5}, {"v": 6}] }, { "v": [{"v": 7}] } ] }`, outputs: map[string]string{ "arr.#.v.0.v": `{ "arr": [ { "v": [{"v": ""}, {"v": 2}] }, { "v": [{"v": ""}, {"v": 4}] }, { "v": [{"v": ""}, {"v": 6}] }, { "v": [{"v": ""}] } ] }`, "arr.#.v.1.v": `{ "arr": [ { "v": [{"v": 1}, {"v": ""}] }, { "v": [{"v": 3}, {"v": ""}] }, { "v": [{"v": 5}, {"v": ""}] }, { "v": [{"v": 7}] } ] }`, "arr.#.v.#.v": `{ "arr": [ { "v": [{"v": ""}, {"v": ""}] }, { "v": [{"v": ""}, {"v": ""}] }, { "v": [{"v": ""}, {"v": ""}] }, { "v": [{"v": ""}] } ] }`, "arr.1.v.#.v": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, { "v": [{"v": ""}, {"v": ""}] }, { "v": [{"v": 5}, {"v": 6}] }, { "v": [{"v": 7}] } ] }`, }, }, { input: `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, {}, { "v": [{"v": 5}, {"v": 6}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": 7}] } ] }`, outputs: map[string]string{ "arr.#.v.0.v": `{ "arr": [ { "v": [{"v": ""}, {"v": 2}] }, {}, { "v": [{"v": ""}, {"v": 6}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": ""}] } ] }`, "arr.#.v.1.v": `{ "arr": [ { "v": [{"v": 1}, {"v": ""}] }, {}, { "v": [{"v": 5}, {"v": ""}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": 7}] } ] }`, "arr.#.v.#.v": `{ "arr": [ { "v": [{"v": ""}, {"v": ""}] }, {}, { "v": [{"v": ""}, {"v": ""}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": ""}] } ] }`, "arr.1.v.#.v": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, {}, { "v": [{"v": 5}, {"v": 6}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": 7}] } ] }`, "arr.#": `{ "arr": [ "", "", "", "", "", "", "" ] }`, "arr.#.v": `{ "arr": [ { "v": "" }, {}, { "v": "" }, { "foo": "bar" }, { "v": "" }, { "v": "" }, { "v": "" } ] }`, "arr.#.v.#": `{ "arr": [ { "v": ["", ""] }, {}, { "v": ["", ""] }, { "foo": "bar" }, { "v": [] }, { "v": [""] }, { "v": [""] } ] }`, }, }, { input: `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, {}, { "v": [{"v": 5}, {"v": 6}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": 7}] } ] }`, outputs: map[string]string{ "arr.#.v.#(v>=3).v": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, {}, { "v": [{"v": ""}, {"v": 6}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": ""}] } ] }`, "arr.#.v.#(v>=3)#": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, {}, { "v": ["", ""] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [""] } ] }`, "arr.#.v.#(v>=3)#.v": `{ "arr": [ { "v": [{"v": 1}, {"v": 2}] }, {}, { "v": [{"v": ""}, {"v": ""}] }, { "foo": "bar" }, { "v": [] }, { "v": [{"foo": "bar"}] }, { "v": [{"v": ""}] } ] }`, }, }, { input: `{ "results": [ {}, { "vulns": [ { "id": "GHSA-9f46-5r25-5wfm", "modified": "2024-02-16T08:21:35.601880Z" } ] }, {}, {}, {} ] }`, outputs: map[string]string{ "results.#.vulns.#.modified": `{ "results": [ {}, { "vulns": [ { "id": "GHSA-9f46-5r25-5wfm", "modified": "" } ] }, {}, {}, {} ] }`, }, }, } for i, tt := range tests { paths := slices.AppendSeq(make([]string, 0, len(tt.outputs)), maps.Keys(tt.outputs)) slices.Sort(paths) for _, path := range paths { t.Run(fmt.Sprintf("%d-%s", i, path), func(t *testing.T) { t.Parallel() got := ReplaceJSONInput(t, tt.input, path, matcher) if !gjson.Valid(got) { t.Fatalf("Output not valid: \n%s", got) } if !gjson.Valid(tt.outputs[path]) { t.Fatalf("Want field is not valid JSON: \n%s", tt.outputs[path]) } var wantPretty bytes.Buffer var gotPretty bytes.Buffer _ = json.Indent(&wantPretty, []byte(tt.outputs[path]), "", " ") _ = json.Indent(&gotPretty, []byte(got), "", " ") if diff := cmp.Diff(wantPretty.String(), gotPretty.String()); diff != "" { t.Errorf("ReplaceJSONInput() diff (-want +got): %s", diff) } }) } } } func Test_replaceJSONInput_More(t *testing.T) { t.Parallel() // A nested JSON structure with arrays nestedArray := `{ "items": [ { "subStruct": { "subitems": [ { "anotherSubStruct": "original value 1" }, { "anotherSubStruct": "original value 2" } ] } }, { "subStruct": { "subitems": [ { "anotherSubStruct": "original value 3" }, { "anotherSubStruct": "original value 4" } ] } } ] }` // A simple JSON structure simpleStruct := `{ "test": { "field": "original value" } }` type args struct { jsonInput string path string matcher func(toReplace gjson.Result) any } tests := []struct { name string args args want string }{ { name: "Nested_json_replacement", args: args{ jsonInput: nestedArray, path: "items.#.subStruct.subitems.#.anotherSubStruct", matcher: func(_ gjson.Result) any { return "" }, }, want: `{ "items": [ { "subStruct": { "subitems": [ { "anotherSubStruct": "" }, { "anotherSubStruct": "" } ] } }, { "subStruct": { "subitems": [ { "anotherSubStruct": "" }, { "anotherSubStruct": "" } ] } } ] }`, }, { name: "simple_json_replacement", args: args{ jsonInput: simpleStruct, path: "test.field", matcher: func(_ gjson.Result) any { return "" }, }, want: `{ "test": { "field": "" } }`, }, { name: "nested_json_array_element_replacement", args: args{ jsonInput: nestedArray, path: "items.#.subStruct.subitems.#", matcher: func(res gjson.Result) any { return res.Get("anotherSubStruct").Value() }, }, want: `{ "items": [ { "subStruct": { "subitems": [ "original value 1", "original value 2" ] } }, { "subStruct": { "subitems": [ "original value 3", "original value 4" ] } } ] }`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := ReplaceJSONInput(t, tt.args.jsonInput, tt.args.path, tt.args.matcher) if !gjson.Valid(got) { t.Fatalf("Output not valid: \n%s", got) } if !gjson.Valid(tt.want) { t.Fatalf("Want field is not valid JSON: \n%s", tt.want) } var wantPretty bytes.Buffer var gotPretty bytes.Buffer _ = json.Indent(&wantPretty, []byte(tt.want), "", "\t") _ = json.Indent(&gotPretty, []byte(got), "", "\t") if diff := cmp.Diff(wantPretty.String(), gotPretty.String()); diff != "" { t.Errorf("ReplaceJSONInput() diff (-want +got): %s", diff) } }) } }