diff --git a/src/main/java/com/endava/cats/factory/FuzzingDataFactory.java b/src/main/java/com/endava/cats/factory/FuzzingDataFactory.java index 6e9398a42..8ab6dbb8c 100644 --- a/src/main/java/com/endava/cats/factory/FuzzingDataFactory.java +++ b/src/main/java/com/endava/cats/factory/FuzzingDataFactory.java @@ -11,6 +11,7 @@ import com.endava.cats.model.generator.OpenAPIModelGenerator; import com.endava.cats.openapi.OpenApiUtils; import com.endava.cats.util.CatsModelUtils; +import com.endava.cats.util.JsonSet; import com.endava.cats.util.JsonUtils; import com.endava.cats.util.KeyValuePair; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -570,7 +571,7 @@ private List buildArray(List singleElements) { private List addNewCombination(JsonElement jsonElement) { Set result = new TreeSet<>(); Deque stack = new ArrayDeque<>(); - Set visited = new HashSet<>(); + JsonSet visited = new JsonSet(); stack.push(jsonElement); @@ -595,8 +596,13 @@ private List addNewCombination(JsonElement jsonElement) { result.clear(); anyOfOrOneOf.forEach((key, value) -> - interimCombinationList.forEach(payload -> - result.add(JsonUtils.createValidOneOfAnyOfNode(payload, pathKey, key, String.valueOf(value), anyOfOrOneOf.keySet())))); + interimCombinationList.forEach(payload -> { + if (payload.contains(ANY_OF) || payload.contains(ONE_OF)) { + result.add(JsonUtils.createValidOneOfAnyOfNode(payload, pathKey, key, String.valueOf(value), anyOfOrOneOf.keySet())); + } else { + result.add(payload); + } + })); // Add elements to the stack for further processing result.stream() diff --git a/src/main/java/com/endava/cats/util/JsonSet.java b/src/main/java/com/endava/cats/util/JsonSet.java new file mode 100644 index 000000000..71b8e8975 --- /dev/null +++ b/src/main/java/com/endava/cats/util/JsonSet.java @@ -0,0 +1,127 @@ +package com.endava.cats.util; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * A simple collection backed by a Set that stores unique Jsons. A Json is considered unique if it has a different set of keys. + */ +public class JsonSet { + + private final Set set; + + /** + * Creates a new JsonSet instance. + */ + public JsonSet() { + this.set = new HashSet<>(); + } + + /** + * Adds a new Json to the set. + * + * @param jsonString the Json to be added + * @return true if the Json was added, false otherwise + */ + public boolean add(String jsonString) { + return set.add(new JsonKeyWrapper(jsonString)); + } + + /** + * Checks if the set contains a Json. + * + * @param jsonString the Json to be checked + * @return true if the Json is in the set, false otherwise + */ + public boolean contains(String jsonString) { + return set.contains(new JsonKeyWrapper(jsonString)); + } + + /** + * Returns the size of the set. + * + * @return the size of the set + */ + public int size() { + return set.size(); + } + + private static class JsonKeyWrapper { + private final Set keyTypeSet; + + public JsonKeyWrapper(String jsonString) { + if (JsonUtils.isValidJson(jsonString)) { + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + this.keyTypeSet = extractKeyTypes(jsonObject); + } else { + this.keyTypeSet = new HashSet<>(); + this.keyTypeSet.add(jsonString); + } + } + + private Set extractKeyTypes(JsonObject jsonObject) { + Set keyTypes = new HashSet<>(); + for (String key : jsonObject.keySet()) { + JsonElement value = jsonObject.get(key); + if (value.isJsonObject()) { + keyTypes.add(key + ":object"); + keyTypes.addAll(extractKeyTypes(value.getAsJsonObject())); + } else if (value.isJsonArray()) { + keyTypes.add(key + ":array"); + keyTypes.addAll(extractArrayKeyTypes(value.getAsJsonArray())); + } else if (value.isJsonPrimitive()) { + if (value.getAsJsonPrimitive().isBoolean()) { + keyTypes.add(key + ":boolean"); + } else if (value.getAsJsonPrimitive().isNumber()) { + keyTypes.add(key + ":number"); + } else if (value.getAsJsonPrimitive().isString()) { + keyTypes.add(key + ":string"); + } + } else { + keyTypes.add(key + ":null"); + } + } + return keyTypes; + } + + private Set extractArrayKeyTypes(com.google.gson.JsonArray jsonArray) { + Set keyTypes = new HashSet<>(); + for (JsonElement element : jsonArray) { + if (element.isJsonObject()) { + keyTypes.addAll(extractKeyTypes(element.getAsJsonObject())); + } else if (element.isJsonArray()) { + keyTypes.addAll(extractArrayKeyTypes(element.getAsJsonArray())); + } else if (element.isJsonPrimitive()) { + if (element.getAsJsonPrimitive().isBoolean()) { + keyTypes.add("array_item:boolean"); + } else if (element.getAsJsonPrimitive().isNumber()) { + keyTypes.add("array_item:number"); + } else if (element.getAsJsonPrimitive().isString()) { + keyTypes.add("array_item:string"); + } + } else { + keyTypes.add("array_item:null"); + } + } + return keyTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JsonKeyWrapper that = (JsonKeyWrapper) o; + return Objects.equals(keyTypeSet, that.keyTypeSet); + } + + @Override + public int hashCode() { + return Objects.hash(keyTypeSet); + } + } +} diff --git a/src/test/java/com/endava/cats/factory/FuzzingDataFactoryTest.java b/src/test/java/com/endava/cats/factory/FuzzingDataFactoryTest.java index c67dc493a..54ed46c66 100644 --- a/src/test/java/com/endava/cats/factory/FuzzingDataFactoryTest.java +++ b/src/test/java/com/endava/cats/factory/FuzzingDataFactoryTest.java @@ -302,7 +302,7 @@ void shouldProperlyGenerateFromArrayWithAnyOfElements() throws Exception { Mockito.when(processingArguments.getLimitXxxOfCombinations()).thenReturn(50); List dataList = setupFuzzingData("/api/v1/studies", "src/test/resources/prolific.yaml"); - Assertions.assertThat(dataList).hasSize(31); + Assertions.assertThat(dataList).hasSize(29); Assertions.assertThat(dataList.stream().map(FuzzingData::getPayload).toList()) .filteredOn(payload -> payload.contains("ANY_OF") || payload.contains("ONE_OF") || payload.contains("ALL_OF")) .hasSize(1); @@ -320,7 +320,7 @@ void shouldLimitXxxCombinationsWhenCombinationsExceedArgument() throws Exception Assertions.assertThat(dataList).hasSize(100); Assertions.assertThat(dataList.stream().map(FuzzingData::getPayload).toList()) .filteredOn(payload -> payload.contains("ANY_OF") || payload.contains("ONE_OF") || payload.contains("ALL_OF")) - .hasSize(3); + .hasSize(0); FuzzingData firstData = dataList.getFirst(); boolean isActionsArray = JsonUtils.isArray(firstData.getPayload(), "$.subject-profile.claims"); @@ -337,10 +337,10 @@ void shouldGenerateCombinationWhenXxxArraysAndSimpleTypes() throws IOException { .noneMatch(payload -> payload.contains("ANY_OF") || payload.contains("ONE_OF") || payload.contains("ALL_OF")); FuzzingData data = dataList.get(4); - Object createPermision = JsonUtils.getVariableFromJson(data.getPayload(), "$.permissions.create[0]"); + Object createPermission = JsonUtils.getVariableFromJson(data.getPayload(), "$.permissions.create[0]"); Object updatePermission = JsonUtils.getVariableFromJson(data.getPayload(), "$.permissions.update[0]"); - Assertions.assertThat(createPermision).asString().isEqualTo("database"); + Assertions.assertThat(createPermission).asString().isEqualTo("database"); Assertions.assertThat(updatePermission).asString().isEqualTo("database"); } @@ -367,7 +367,7 @@ void shouldGenerateCombinationsWhenXxxAsInlineSchemas() throws IOException { void shouldProperlyParseRootAllOfAndOneOfElements() throws Exception { List dataList = setupFuzzingData("/payouts", "src/test/resources/token.yml"); - Assertions.assertThat(dataList).hasSize(11); + Assertions.assertThat(dataList).hasSize(10); Assertions.assertThat(dataList.stream().map(FuzzingData::getPayload).toList()) .noneMatch(payload -> payload.contains("ANY_OF") || payload.contains("ONE_OF") || payload.contains("ALL_OF")); FuzzingData firstData = dataList.getFirst();