Skip to content

Commit

Permalink
Feature/sip 1199 advanced body validation (#236)
Browse files Browse the repository at this point in the history
* - Refactoring CamelBodyValidatorTest

* - Refactoring CamelBodyValidatorTest: formatting

* - Advanced body validator with XML and JSON comparators

* - changelogs and docs

* - Reverting message properties changes

* correcting sip test kit version

* adding commons-io dependency

* adding commons-io dependency

* adding jaxb-api dependency

* Excluding transitive dependencies that cause licence issue

* Excluding transitive dependencies that cause licence issue

* Upgrade jaxb-api to 2.3.1 to solve license issue

---------

Co-authored-by: H.Schröder <[email protected]>
Co-authored-by: Leto Bukarica <[email protected]>
  • Loading branch information
3 people authored Sep 21, 2023
1 parent efc0742 commit 96e675c
Show file tree
Hide file tree
Showing 18 changed files with 557 additions and 84 deletions.
5 changes: 5 additions & 0 deletions changelogs/feature/advanced_body_validation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"author": "vladiIkor",
"pullrequestId": "236",
"message": "Support for XML and JSON tree comparison."
}
5 changes: 3 additions & 2 deletions docs-snapshot/test-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ we want to validate. It could be the entering endpoint of the adapter, for examp
adapter, or it could be any external system (mocked) endpoint, where we can validate the input that mocked endpoint has received;
this way we could validate, for instance, if a properly transformed file reached the outgoing FTP endpoint.
Validation is performed on two levels, _*body*_ - where the data is validated and _*headers*_ - where metadata is validated. Body
validation is performed as plain text comparison, binary payload is not yet supported. Headers comparison is comparing
textual key value maps. Both body and header validation support regex pattern as expected value.
validation is performed as plain text comparison, XML comparison and JSON comparison. Binary payload is not yet supported.
Headers comparison is comparing textual key value maps. Both body and header validation support regex pattern as expected value.
Given that all SIP mocks are internal, meaning that the actual endpoint is replaced with the mock, any URI options defined
on the mock will not apply and behavior produced by them is not possible to verify.

Expand Down Expand Up @@ -97,6 +97,7 @@ test-case-definitions:
headers:
header-key: "Regex expression (java) which will be compered to the header key value from request which arrived on the adapter"
```

Given that body can vary in length, it can be set as a reference to a file where the content resides. For example:
``` yaml
WHEN-execute:
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<asm.version>9.4</asm.version> <!-- Spring is using 9.1, Cxf is using 9.4, when Spring upgrades to 9.4, then explicit dependency can be removed -->
<cxf.version>4.0.2</cxf.version> <!-- keep in sync with camel-dependencies cxf-version property -->
<snakeyaml.version>2.0</snakeyaml.version>
<jaxb-api.version>2.3.1</jaxb-api.version>
<!-- maven dependancies -->
<maven-core.version>3.8.6</maven-core.version>
<maven-project-dependecies.version>3.8.4</maven-project-dependecies.version>
Expand Down Expand Up @@ -262,6 +263,11 @@


<!-- additional dependencies-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-api.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
Expand Down
26 changes: 26 additions & 0 deletions sip-test-kit/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,32 @@
<name>SIP Test Kit</name>

<dependencies>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<exclusions>
<exclusion>
<groupId>javax.xml.stream</groupId>
<artifactId>stax-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-matchers</artifactId>
</dependency>
<dependency>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</dependency>
<dependency>
<groupId>de.ikor.sip.foundation</groupId>
<artifactId>sip-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl;

import static java.lang.String.format;
import static java.util.stream.Collectors.groupingBy;
import static org.apache.camel.support.MessageHelper.extractBodyAsString;
import static org.apache.camel.support.MessageHelper.resetStreamCache;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNoneBlank;

import de.ikor.sip.foundation.testkit.util.RegexUtil;
import de.ikor.sip.foundation.testkit.workflow.thenphase.result.ValidationResult;
import de.ikor.sip.foundation.testkit.workflow.thenphase.validator.ExchangeValidator;
import lombok.AllArgsConstructor;
import de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators.*;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.camel.Exchange;
import org.springframework.stereotype.Component;

/** Validator for body of a request in Camel */
@Slf4j
@Component
@AllArgsConstructor
public class CamelBodyValidator implements ExchangeValidator {
public static final String BODY_VALIDATION_UNSUCCESSFUL = "Body validation unsuccessful";
public static final String BODY_VALIDATION_SUCCESSFUL = "Body validation successful";
private final List<StringComparator> comparators =
List.of(new XMLComparator(), new JsonComparator(), new RegexComparator());

/**
* Invokes compare body content
Expand All @@ -25,16 +35,64 @@ public class CamelBodyValidator implements ExchangeValidator {
@Override
public ValidationResult execute(Exchange actualResult, Exchange expectedResponse) {
resetStreamCache(actualResult.getMessage());
boolean result =
RegexUtil.compare(
extractBodyAsString(expectedResponse.getMessage()),
extractBodyAsString(actualResult.getMessage()));
return new ValidationResult(
result, result ? "Body validation successful" : "Body validation unsuccessful");
String expected = extractBodyAsString(expectedResponse.getMessage());
String actual = extractBodyAsString(actualResult.getMessage());

if (!isMatchPossible(actual, expected)) {
return new ValidationResult(false, BODY_VALIDATION_UNSUCCESSFUL);
}

Map<Boolean, List<ComparatorResult>> results =
comparators.stream()
.map(comparator -> safeCompare(expected, actual, comparator))
.filter(comparatorResult -> comparatorResult.getStatus() != null)
.collect(groupingBy(ComparatorResult::getStatus));

return isValidationSuccess(results)
? new ValidationResult(true, BODY_VALIDATION_SUCCESSFUL)
: results.get(false).stream()
.map(this::toValidationResult)
.findFirst()
.orElse(new ValidationResult(false, BODY_VALIDATION_UNSUCCESSFUL));
}

@Override
public boolean isApplicable(Exchange executionResult, Exchange expectedResponse) {
return expectedResponse != null && extractBodyAsString(expectedResponse.getMessage()) != null;
}

private ValidationResult toValidationResult(ComparatorResult comparatorResult) {
return new ValidationResult(false, getFailureDescription(comparatorResult));
}

private String getFailureDescription(ComparatorResult comparatorResult) {
String descriptionFromComparator =
format("%s: %s", BODY_VALIDATION_UNSUCCESSFUL, comparatorResult.getFailureDescription());
return comparatorResult.getFailureDescription() == null
? BODY_VALIDATION_UNSUCCESSFUL
: descriptionFromComparator;
}

private static ComparatorResult safeCompare(
String expected, String actual, StringComparator comparator) {
try {
return comparator.compare(expected, actual);
} catch (IncompatibleStringComparator e) {
log.trace(format("Expected: %s %n Actual: %s ", expected, actual), e.getMessage());
return new ComparatorResult();
}
}

private static boolean isValidationSuccess(
Map<Boolean, List<ComparatorResult>> validationResults) {
return validationResults.containsKey(true) && !validationResults.get(true).isEmpty();
}

private boolean isMatchPossible(String actual, String expected) {
return !areSurelyDifferent(actual, expected);
}

private static boolean areSurelyDifferent(String actual, String expected) {
return isNoneBlank(expected) && isBlank(actual);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ComparatorResult {
private Boolean status;
private String failureDescription;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators;

public class IncompatibleStringComparator extends RuntimeException {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators;

import static java.lang.String.format;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

/**
* A comparator for comparing JSON strings, based on Jackson object mapper.
*
* <p>This comparator converts JSON strings into maps and then compares the maps for equality. The
* comparison result provides information about whether the two JSON strings are equal and a
* description of the differences if they are not.
*/
@Slf4j
public class JsonComparator implements StringComparator {
private final ObjectMapper mapper = new ObjectMapper();

/**
* Compares two JSON strings for equality.
*
* @param expected The expected JSON string.
* @param actual The actual JSON string to be compared against the expected.
* @return A {@link ComparatorResult} containing the result of the comparison.
*/
public ComparatorResult compare(String expected, String actual) {
try {
return doCompare(expected, actual);
} catch (Exception e) {
throw new IncompatibleStringComparator();
}
}

private ComparatorResult doCompare(String expected, String actual)
throws JsonProcessingException {
log.trace(format("Comparing .%n Expected: %s %n Actual: %s ", expected, actual));
TypeReference<HashMap<String, Object>> type = new TypeReference<>() {};

Map<String, Object> expectedAsMap = mapper.readValue(expected, type);
Map<String, Object> actualAsMap = mapper.readValue(actual, type);

MapDifference<String, Object> difference = Maps.difference(expectedAsMap, actualAsMap);
return new ComparatorResult(
difference.areEqual(), difference.areEqual() ? null : difference.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators;

import de.ikor.sip.foundation.testkit.util.RegexUtil;

public class RegexComparator implements StringComparator {
@Override
public ComparatorResult compare(String expected, String actual) {
boolean matches = RegexUtil.compare(expected, actual);
return new ComparatorResult(matches, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators;

import de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.CamelBodyValidator;

/**
* SIP abstraction used by Test Kit. Used to implement different comparators in order to provide
* more powerful body validation process. Any implementation of StringComparator should be
* instantiated in {@link CamelBodyValidator} in order to take part in body validation process.
*/
public interface StringComparator {
ComparatorResult compare(String expected, String actual);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators;

import static java.lang.String.format;

import de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators.util.SilentDocumentFactory;
import lombok.extern.slf4j.Slf4j;
import org.xmlunit.builder.DiffBuilder;
import org.xmlunit.diff.DefaultNodeMatcher;
import org.xmlunit.diff.Diff;
import org.xmlunit.diff.ElementSelectors;

/**
* A comparator for comparing XML strings, based on xmlunit library.
*
* <p>This comparator uses the DiffBuilder to compare two XML strings for similarity. The comparison
* result provides information about whether the two XML strings are similar and a description of
* the differences if they are not.
*/
@Slf4j
public class XMLComparator implements StringComparator {

/**
* Compares two XML strings for similarity.
*
* @param expected The expected XML string.
* @param actual The actual XML string to be compared against the expected.
* @return A {@link ComparatorResult} containing the result of the comparison.
*/
public ComparatorResult compare(String expected, String actual) {
try {
return doCompare(expected, actual);
} catch (Exception e) {
throw new IncompatibleStringComparator();
}
}

private ComparatorResult doCompare(String expected, String actual) {
log.trace(format("Comparing xml content.%n Expected: %s %n Actual: %s ", expected, actual));
Diff diff =
DiffBuilder.compare(expected)
.withDocumentBuilderFactory(new SilentDocumentFactory())
.withTest(actual)
.normalizeWhitespace()
.checkForSimilar()
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName))
.build();

return new ComparatorResult(
!diff.hasDifferences(), !diff.hasDifferences() ? null : diff.fullDescription());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators.util;

import com.ctc.wstx.shaded.msv_core.verifier.jaxp.DocumentBuilderFactoryImpl;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import lombok.extern.slf4j.Slf4j;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
* A custom implementation of {@link DocumentBuilderFactoryImpl} that provides a silent document
* builder. This factory produces document builders that suppress errors and log them instead of
* throwing them directly.
*/
public class SilentDocumentFactory extends DocumentBuilderFactoryImpl {
/**
* Creates a new instance of a silent document builder.
*
* @return a new instance of {@link DocumentBuilder} with a custom error handler.
* @throws ParserConfigurationException if a DocumentBuilder cannot be created which satisfies the
* configuration requested.
*/
@Override
public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilder documentBuilder = super.newDocumentBuilder();
documentBuilder.setErrorHandler(new SilentErrorHandler());
return documentBuilder;
}

/**
* A custom error handler that logs SAX parse exceptions silently without throwing them. This
* handler is intended to be used with the {@link DocumentBuilder} produced by {@link
* SilentDocumentFactory}.
*/
@Slf4j
private static class SilentErrorHandler implements ErrorHandler {
@Override
public void warning(SAXParseException exception) throws SAXException {
log.debug("WARNING: " + exception.getMessage(), exception);
}

@Override
public void error(SAXParseException exception) throws SAXException {
log.debug("ERROR: " + exception.getMessage(), exception);
}

@Override
public void fatalError(SAXParseException exception) throws SAXException {
log.debug("ERROR: " + exception.getMessage(), exception);
throw exception;
}
}
}
Loading

0 comments on commit 96e675c

Please sign in to comment.