de.gematik.test.tiger.lib.json.JsonChecker Maven / Gradle / Ivy
/*
* Copyright 2024 gematik GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package de.gematik.test.tiger.lib.json;
import static org.assertj.core.api.Assertions.assertThat;
import groovy.util.logging.Slf4j;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import lombok.SneakyThrows;
import net.serenitybdd.annotations.Step;
import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.lang3.StringUtils;
import org.assertj.core.api.Assertions;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.skyscreamer.jsonassert.*;
import org.skyscreamer.jsonassert.comparator.CustomComparator;
/**
* values will be first checked for containing "${json-unit.ignore}" then for equals and finally for
* regex matches
*
* JSON object attributes starting with four underscores "____" are optional and allow the oracle
* string to contain attributes to be checked for value ONLY if it exists in the test JSON
*/
@Slf4j
public class JsonChecker {
public static final String IGNORE_JSON_VALUE = "${json-unit.ignore}";
private static final String OPTIONAL_MARKER = "____";
private static final String NULL_MARKER = "$NULL";
@Step
public void compareJsonStrings(
final String jsonStr, final String oracleStr, boolean checkExtraAttributes) {
JSONTokener jsonTokener = new JSONTokener(jsonStr);
JSONTokener oracleTokener = new JSONTokener(oracleStr);
final Object jsonValue = jsonTokener.nextValue();
final Object oracleValue = oracleTokener.nextValue();
compareJsonStrings(jsonValue, oracleValue, checkExtraAttributes);
}
public void compareJsonStrings(
final Object jsonValue, final Object oracleValue, boolean checkExtraAttributes) {
if ((NULL_MARKER.equals(oracleValue) && jsonValue == JSONObject.NULL)
|| (NULL_MARKER.equals(jsonValue) && oracleValue == JSONObject.NULL)) {
return;
}
if (!jsonValue.getClass().equals(oracleValue.getClass())) {
throw new JsonCheckerMismatchException(
String.format(
"Could not compare %s to %s: Different types!",
jsonValue.getClass().getSimpleName(), oracleValue.getClass().getSimpleName()));
}
if (jsonValue instanceof JSONObject jsonObject) {
assertJsonObjectShouldMatchOrContainInAnyOrder(
jsonObject, (JSONObject) oracleValue, checkExtraAttributes);
} else if (jsonValue instanceof JSONArray jsonArray) {
assertJsonArrayShouldMatchInAnyOrder(
jsonArray.toString(), oracleValue.toString(), checkExtraAttributes);
} else {
compareValues(jsonValue, oracleValue);
}
}
@Step
public void assertJsonArrayShouldMatchInAnyOrder(final String json, final String oracle) {
assertJsonArrayShouldMatchInAnyOrder(json, oracle, true);
}
public void assertJsonArrayShouldMatchInAnyOrder(
final String json, final String oracle, boolean checkExtraAttributes) {
JSONAssert.assertEquals(
oracle,
json,
new CustomComparator(
JSONCompareMode.LENIENT,
new Customization(
"***",
(testJson, oracleJson) -> {
if (testJson instanceof JSONObject) {
assertJsonObjectShouldMatchOrContainInAnyOrder(
testJson.toString(), oracleJson.toString(), checkExtraAttributes);
return true;
} else if (testJson instanceof JSONArray) {
assertJsonArrayShouldMatchInAnyOrder(
testJson.toString(), oracleJson.toString(), checkExtraAttributes);
return true;
} else {
// return true if its json ignore value in oracle or if values are equal
return (IGNORE_JSON_VALUE.equals(oracleJson)
|| testJson.toString().equals(oracleJson.toString()))
// else check if the values match
|| testJson.toString().matches(oracleJson.toString());
}
})));
}
@Step
public void assertJsonObjectShouldMatchOrContainInAnyOrder(
final String jsonStr, final String oracleStr, boolean checkExtraAttributes) {
final JSONObject json = convertToJsonObject(jsonStr);
final JSONObject oracle = convertToJsonObject(oracleStr);
assertJsonObjectShouldMatchOrContainInAnyOrder(json, oracle, checkExtraAttributes);
}
public void assertJsonObjectShouldMatchOrContainInAnyOrder(
final JSONObject json, final JSONObject oracle, boolean checkExtraAttributes) {
try {
for (String oracleKey : oracle.keySet()) {
if (keyNotContainedInSetOrOptional(oracleKey, json.keySet())) {
throw new JsonCheckerMismatchException(
"Expected JSON to have key '"
+ oracleKey
+ "', but only found keys '"
+ json.keySet()
+ "'");
}
}
if (checkExtraAttributes) {
// check json keys are all in oracle (either as name or as ____name
final Optional checkerMismatchException =
json.keySet().stream()
.filter(key -> keyNotContainedInSetOrOptional(key, oracle.keySet()))
.findAny()
.map(
key ->
new JsonCheckerMismatchException(
"EXTRA Key " + key + " detected in received in JSON"));
if (checkerMismatchException.isPresent()) {
throw checkerMismatchException.get();
}
}
compareAllAttributes(json, oracle);
} catch (final NoSuchMethodError nsme) {
throw new JsonCheckerMismatchException(
dumpComparisonBetween(
"JSON does not match!\nExpected:\n%s\n\n--------\n\nReceived:\n%s",
oracle.toString(2), json.toString(2)),
nsme);
}
}
private boolean keyNotContainedInSetOrOptional(String oracleKey, Set keySet) {
if (oracleKey.startsWith(OPTIONAL_MARKER)) {
return false;
}
return keySet.stream()
.map(key -> StringUtils.stripStart(key, OPTIONAL_MARKER))
.noneMatch(oracleKey::equals);
}
private Optional