Skip to content

Commit 0e41391

Browse files
authored
test(apitester): rewrite json replacement logic to better handle arrays and existence (#4306)
Currently, attempting to replace properties within arrays on objects that don't always have the same structure all the way down can result in undesired behaviours such as the property being added to all objects within arrays on the path, or only half of the potential matches being replaced. While the problem is a bit awkward to describe, the solution is not: now we fully unroll a replacement path, and apply the replacement logic on each resulting path. i.e.with the object `{ "arr": [1, 2, 3] }`, we unroll the path `arr.#` to `[]string{"arr.0", "arr.1", "arr.2"}`.
1 parent 72bd2be commit 0e41391

File tree

2 files changed

+494
-45
lines changed

2 files changed

+494
-45
lines changed

tools/apitester/internal/jsonreplace/jsonreplace.go

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,66 +25,64 @@ func DoBytes(t *testing.T, json []byte, rules []Rule) []byte {
2525
return json
2626
}
2727

28-
// replaceJSONInput takes a gjson path and replaces all elements the path matches with the output of matcher
29-
func replaceJSONInput(t *testing.T, jsonInput []byte, path string, matcher func(toReplace gjson.Result) any) []byte {
28+
func expandArrayPaths(t *testing.T, jsonInput []byte, path string) []string {
3029
t.Helper()
3130

32-
pathArray := []string{}
31+
// split on the first intermediate #, if present
32+
pathToArray, restOfPath, hasArrayPlaceholder := strings.Cut(path, ".#.")
33+
34+
// if there is no intermediate placeholder, check for (and cut) a terminal one
35+
if !hasArrayPlaceholder {
36+
pathToArray, hasArrayPlaceholder = strings.CutSuffix(path, ".#")
37+
}
38+
39+
// if there are no array placeholders in the path, just return it
40+
if !hasArrayPlaceholder {
41+
return []string{path}
42+
}
43+
44+
r := gjson.GetBytes(jsonInput, pathToArray)
3345

34-
// If there are more than 2 #, sjson cannot replace them directly. Iterate out all individual entries
35-
if strings.Contains(path, "#") {
36-
// Get the path ending with #
37-
// E.g. results.#.packages.#.vulnerabilities => results.#.packages.#
38-
numOfEntriesPath := path[:strings.LastIndex(path, "#")+1]
39-
// This returns a potentially nested array of array lengths
40-
numOfEntries := gjson.GetBytes(jsonInput, numOfEntriesPath)
46+
// skip properties that are not arrays
47+
if !r.IsArray() {
48+
return []string{}
49+
}
50+
51+
// if property exists and is actually an array, build out the path to each item
52+
// within that array
53+
paths := make([]string, 0, len(r.Array()))
54+
55+
for i := range r.Array() {
56+
static := pathToArray + "." + strconv.Itoa(i)
4157

42-
// Use it to build up a list of concrete paths
43-
buildSJSONPaths(t, &pathArray, path, numOfEntries)
44-
} else {
45-
pathArray = append(pathArray, path)
58+
if restOfPath != "" {
59+
static += "." + restOfPath
60+
}
61+
paths = append(paths, expandArrayPaths(t, jsonInput, static)...)
4662
}
4763

64+
return paths
65+
}
66+
67+
// replaceJSONInput takes a gjson path and replaces all elements the path matches with the output of matcher
68+
func replaceJSONInput(t *testing.T, jsonInput []byte, path string, replacer func(toReplace gjson.Result) any) []byte {
69+
t.Helper()
70+
4871
var err error
4972
json := jsonInput
50-
for _, pathElem := range pathArray {
73+
for _, pathElem := range expandArrayPaths(t, jsonInput, path) {
5174
res := gjson.GetBytes(jsonInput, pathElem)
5275

5376
if !res.Exists() {
5477
continue
5578
}
5679

57-
json, err = sjson.SetBytesOptions(json, pathElem, matcher(res), &sjson.Options{Optimistic: true})
80+
// optimistically replace the element, since we know at this point it does exist
81+
json, err = sjson.SetBytesOptions(json, pathElem, replacer(res), &sjson.Options{Optimistic: true})
5882
if err != nil {
5983
t.Fatalf("failed to set element")
6084
}
6185
}
6286

6387
return json
6488
}
65-
66-
func buildSJSONPaths(t *testing.T, pathToBuild *[]string, path string, structure gjson.Result) {
67-
t.Helper()
68-
69-
if structure.IsArray() {
70-
// More nesting to go
71-
for i, res := range structure.Array() {
72-
buildSJSONPaths(
73-
t,
74-
pathToBuild,
75-
// Replace the first # with actual index
76-
strings.Replace(path, "#", strconv.Itoa(i), 1),
77-
res,
78-
)
79-
}
80-
} else {
81-
// Otherwise assume it is a number
82-
if strings.Count(path, "#") != 1 {
83-
t.Fatalf("programmer error: there should only be 1 # left")
84-
}
85-
for i2 := range int(structure.Int()) {
86-
newPath := strings.Replace(path, "#", strconv.Itoa(i2), 1)
87-
*pathToBuild = append(*pathToBuild, newPath)
88-
}
89-
}
90-
}

0 commit comments

Comments
 (0)