Skip to content

Commit

Permalink
fix: Fix issue when nested oneOf/anyOf combinations was considering d…
Browse files Browse the repository at this point in the history
…uplicate payload structures
  • Loading branch information
en-milie committed Sep 7, 2024
1 parent ce5bc80 commit 60ed30c
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 8 deletions.
12 changes: 9 additions & 3 deletions src/main/java/com/endava/cats/factory/FuzzingDataFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -570,7 +571,7 @@ private List<String> buildArray(List<String> singleElements) {
private List<String> addNewCombination(JsonElement jsonElement) {
Set<String> result = new TreeSet<>();
Deque<JsonElement> stack = new ArrayDeque<>();
Set<String> visited = new HashSet<>();
JsonSet visited = new JsonSet();

stack.push(jsonElement);

Expand All @@ -595,8 +596,13 @@ private List<String> 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()
Expand Down
127 changes: 127 additions & 0 deletions src/main/java/com/endava/cats/util/JsonSet.java
Original file line number Diff line number Diff line change
@@ -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<JsonKeyWrapper> 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<String> 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<String> extractKeyTypes(JsonObject jsonObject) {
Set<String> 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<String> extractArrayKeyTypes(com.google.gson.JsonArray jsonArray) {
Set<String> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ void shouldProperlyGenerateFromArrayWithAnyOfElements() throws Exception {
Mockito.when(processingArguments.getLimitXxxOfCombinations()).thenReturn(50);
List<FuzzingData> 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);
Expand All @@ -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");
Expand All @@ -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");
}

Expand All @@ -367,7 +367,7 @@ void shouldGenerateCombinationsWhenXxxAsInlineSchemas() throws IOException {
void shouldProperlyParseRootAllOfAndOneOfElements() throws Exception {
List<FuzzingData> 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();
Expand Down

0 comments on commit 60ed30c

Please sign in to comment.