From 96e675c709e9e28cda7c7712a97948069f959d85 Mon Sep 17 00:00:00 2001 From: Vladimir Tucakovic Date: Thu, 21 Sep 2023 14:53:04 +0200 Subject: [PATCH] Feature/sip 1199 advanced body validation (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - 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 <47591017+HaukeSchroederIkor@users.noreply.github.com> Co-authored-by: Leto Bukarica --- .../feature/advanced_body_validation.json | 5 + docs-snapshot/test-kit.md | 5 +- pom.xml | 6 + sip-test-kit/pom.xml | 26 ++ .../validator/impl/CamelBodyValidator.java | 76 +++++- .../impl/comparators/ComparatorResult.java | 13 + .../IncompatibleStringComparator.java | 3 + .../impl/comparators/JsonComparator.java | 52 ++++ .../impl/comparators/RegexComparator.java | 11 + .../impl/comparators/StringComparator.java | 12 + .../impl/comparators/XMLComparator.java | 51 ++++ .../util/SilentDocumentFactory.java | 54 ++++ .../impl/CamelBodyValidatorTest.java | 246 ++++++++++++------ .../src/test/resources/logback-test.xml | 15 ++ .../test data/xml/attributes_reordered.xml | 17 ++ .../test data/xml/fieldsReordered.xml | 15 ++ .../test data/xml/namespaceRelabeled.xml | 17 ++ .../test/resources/test data/xml/original.xml | 17 ++ 18 files changed, 557 insertions(+), 84 deletions(-) create mode 100644 changelogs/feature/advanced_body_validation.json create mode 100644 sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/ComparatorResult.java create mode 100644 sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/IncompatibleStringComparator.java create mode 100644 sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/JsonComparator.java create mode 100644 sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/RegexComparator.java create mode 100644 sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/StringComparator.java create mode 100644 sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/XMLComparator.java create mode 100644 sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/util/SilentDocumentFactory.java create mode 100644 sip-test-kit/src/test/resources/logback-test.xml create mode 100644 sip-test-kit/src/test/resources/test data/xml/attributes_reordered.xml create mode 100644 sip-test-kit/src/test/resources/test data/xml/fieldsReordered.xml create mode 100644 sip-test-kit/src/test/resources/test data/xml/namespaceRelabeled.xml create mode 100644 sip-test-kit/src/test/resources/test data/xml/original.xml diff --git a/changelogs/feature/advanced_body_validation.json b/changelogs/feature/advanced_body_validation.json new file mode 100644 index 0000000000..7c8c90d7f4 --- /dev/null +++ b/changelogs/feature/advanced_body_validation.json @@ -0,0 +1,5 @@ +{ + "author": "vladiIkor", + "pullrequestId": "236", + "message": "Support for XML and JSON tree comparison." +} diff --git a/docs-snapshot/test-kit.md b/docs-snapshot/test-kit.md index 90750d492f..87b10b8999 100644 --- a/docs-snapshot/test-kit.md +++ b/docs-snapshot/test-kit.md @@ -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. @@ -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: diff --git a/pom.xml b/pom.xml index e701e205f5..36e7cc40c5 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ 9.4 4.0.2 2.0 + 2.3.1 3.8.6 3.8.4 @@ -262,6 +263,11 @@ + + javax.xml.bind + jaxb-api + ${jaxb-api.version} + com.fasterxml.jackson.datatype jackson-datatype-jsr310 diff --git a/sip-test-kit/pom.xml b/sip-test-kit/pom.xml index e96c9ac816..ae3e9e87b1 100644 --- a/sip-test-kit/pom.xml +++ b/sip-test-kit/pom.xml @@ -15,6 +15,32 @@ SIP Test Kit + + javax.xml.bind + jaxb-api + + + javax.xml.stream + stax-api + + + javax.activation + activation + + + + + org.xmlunit + xmlunit-core + + + org.xmlunit + xmlunit-matchers + + + guava + com.google.guava + de.ikor.sip.foundation sip-core diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidator.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidator.java index 04d0aa082b..4d9b12dfad 100644 --- a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidator.java +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidator.java @@ -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 comparators = + List.of(new XMLComparator(), new JsonComparator(), new RegexComparator()); /** * Invokes compare body content @@ -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> 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> 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); + } } diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/ComparatorResult.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/ComparatorResult.java new file mode 100644 index 0000000000..e0d95f6a0d --- /dev/null +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/ComparatorResult.java @@ -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; +} diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/IncompatibleStringComparator.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/IncompatibleStringComparator.java new file mode 100644 index 0000000000..2d0af6f457 --- /dev/null +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/IncompatibleStringComparator.java @@ -0,0 +1,3 @@ +package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl.comparators; + +public class IncompatibleStringComparator extends RuntimeException {} diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/JsonComparator.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/JsonComparator.java new file mode 100644 index 0000000000..755ad5ca7a --- /dev/null +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/JsonComparator.java @@ -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. + * + *

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> type = new TypeReference<>() {}; + + Map expectedAsMap = mapper.readValue(expected, type); + Map actualAsMap = mapper.readValue(actual, type); + + MapDifference difference = Maps.difference(expectedAsMap, actualAsMap); + return new ComparatorResult( + difference.areEqual(), difference.areEqual() ? null : difference.toString()); + } +} diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/RegexComparator.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/RegexComparator.java new file mode 100644 index 0000000000..eccaf56e61 --- /dev/null +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/RegexComparator.java @@ -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); + } +} diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/StringComparator.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/StringComparator.java new file mode 100644 index 0000000000..a612b92f60 --- /dev/null +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/StringComparator.java @@ -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); +} diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/XMLComparator.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/XMLComparator.java new file mode 100644 index 0000000000..792c1d2e51 --- /dev/null +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/XMLComparator.java @@ -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. + * + *

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()); + } +} diff --git a/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/util/SilentDocumentFactory.java b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/util/SilentDocumentFactory.java new file mode 100644 index 0000000000..246a091808 --- /dev/null +++ b/sip-test-kit/src/main/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/comparators/util/SilentDocumentFactory.java @@ -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; + } + } +} diff --git a/sip-test-kit/src/test/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidatorTest.java b/sip-test-kit/src/test/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidatorTest.java index 7044c1529c..20d6490ff4 100644 --- a/sip-test-kit/src/test/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidatorTest.java +++ b/sip-test-kit/src/test/java/de/ikor/sip/foundation/testkit/workflow/thenphase/validator/impl/CamelBodyValidatorTest.java @@ -1,122 +1,222 @@ package de.ikor.sip.foundation.testkit.workflow.thenphase.validator.impl; +import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; import de.ikor.sip.foundation.testkit.workflow.thenphase.result.ValidationResult; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import org.apache.camel.Exchange; -import org.apache.camel.Message; +import org.apache.commons.io.FileUtils; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.ClassPathResource; @ExtendWith(MockitoExtension.class) class CamelBodyValidatorTest { - + private static final String NAME_JOHN_AGE_30_CAR_NULL_EXAMPLE = + "{\"name\":\"John\", \"age\":30, \"car\":null}"; private static final ValidationResult VALIDATION_RESULT_SUCCESSFUL = new ValidationResult(true, "Body validation successful"); private static final ValidationResult VALIDATION_RESULT_UNSUCCESSFUL = new ValidationResult(false, "Body validation unsuccessful"); - CamelBodyValidator bodyValidatorSubject; - Exchange result; - Exchange expected; - private static final String RESULT = "test"; + private final CamelBodyValidator bodyValidatorSubject = new CamelBodyValidator(); + private Exchange actual; + private Exchange expected; @BeforeEach public void setUp() { - bodyValidatorSubject = new CamelBodyValidator(); - result = mock(Exchange.class, RETURNS_DEEP_STUBS); + // reset mocks + actual = mock(Exchange.class, RETURNS_DEEP_STUBS); expected = mock(Exchange.class, RETURNS_DEEP_STUBS); } - @Test - void When_execute_Expect_Success() { - Message resultMessage = mock(Message.class); - Message expectedMessage = mock(Message.class); - when(result.getMessage()).thenReturn(resultMessage); - when(expected.getMessage()).thenReturn(expectedMessage); - when(resultMessage.getBody()).thenReturn(RESULT); - when(expectedMessage.getBody()).thenReturn(RESULT); - - ValidationResult validationResult = bodyValidatorSubject.execute(result, expected); + @Nested + class ValidatorIsApplicable { + @ParameterizedTest + @ValueSource(strings = {EMPTY, "some value", "null"}) + void When_expectedBodyIsEmptyString_forAnyActualValue(String actualValue) { + when(expected.getMessage().getBody()).thenReturn(EMPTY); + lenient().when(actual.getMessage().getBody()).thenReturn(parseNull(actualValue)); + + boolean isApplicable = bodyValidatorSubject.isApplicable(actual, expected); + + assertThat(isApplicable) + .describedAs( + "Body validator should be applicable if expected has empty string value, regardless of actual value") + .isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {EMPTY, "some value", "null"}) + void When_expectedBodyHasValue_forAnyActualValue(String actualValue) { + when(expected.getMessage().getBody()).thenReturn("any content"); + lenient().when(actual.getMessage().getBody()).thenReturn(parseNull(actualValue)); + + boolean isApplicable = bodyValidatorSubject.isApplicable(actual, expected); + + assertThat(isApplicable) + .describedAs( + "Body validator should be applicable if expected has concrete value, regardless of actual value") + .isTrue(); + } + } - assertEquals(VALIDATION_RESULT_SUCCESSFUL, validationResult); + @Nested + class ValidatorIsNotApplicable { + @ParameterizedTest + @ValueSource(strings = {EMPTY, "some value", "null"}) + void When_expectedBodyIsNull_forAnyActualValue(String actualValue) { + when(expected.getMessage().getBody()).thenReturn(null); + lenient().when(actual.getMessage().getBody()).thenReturn(parseNull(actualValue)); + + boolean isApplicable = bodyValidatorSubject.isApplicable(actual, expected); + + assertThat(isApplicable) + .describedAs( + "Body validator should not be applicable if expected value is null, regardless of actual value") + .isFalse(); + } } - @Test - void When_execute_With_DifferentActualAndExpected_Then_Fail() { - Message resultMessage = mock(Message.class); - Message expectedMessage = mock(Message.class); - when(result.getMessage()).thenReturn(resultMessage); - when(expected.getMessage()).thenReturn(expectedMessage); - when(resultMessage.getBody()).thenReturn(RESULT); - when(expectedMessage.getBody()).thenReturn("null"); + @Nested + class ValidationPasses { + @Test + void When_actualAndExpectedAreTheSame() { + when(actual.getMessage().getBody()).thenReturn("some content"); + when(expected.getMessage().getBody()).thenReturn("some content"); - ValidationResult validationResult = bodyValidatorSubject.execute(result, expected); + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); - assertThat(validationResult).isEqualTo(VALIDATION_RESULT_UNSUCCESSFUL); - } + assertEquals(VALIDATION_RESULT_SUCCESSFUL, validationResult); + } - @Test - void When_execute_With_NullActual_Then_Fail() { - Message resultMessage = mock(Message.class); - Message expectedMessage = mock(Message.class); - when(result.getMessage()).thenReturn(resultMessage); - when(expected.getMessage()).thenReturn(expectedMessage); - when(resultMessage.getBody()).thenReturn("null"); - when(expectedMessage.getBody()).thenReturn("test"); + @ParameterizedTest + @ValueSource(strings = {"null", ""}) + void When_expectedIsEmptyAndActualIsNullOrEmpty(String actualValue) { + when(actual.getMessage().getBody()).thenReturn(parseNull(actualValue)); + when(expected.getMessage().getBody()).thenReturn(EMPTY); - ValidationResult validationResult = bodyValidatorSubject.execute(result, expected); + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); - assertThat(validationResult).isEqualTo(VALIDATION_RESULT_UNSUCCESSFUL); - } + assertThat(validationResult).isEqualTo(VALIDATION_RESULT_SUCCESSFUL); + } - @Test - void When_execute_With_NullActualAndEmptyExpected_Then_Success() { - Message resultMessage = mock(Message.class); - Message expectedMessage = mock(Message.class); - when(result.getMessage()).thenReturn(resultMessage); - when(expected.getMessage()).thenReturn(expectedMessage); - when(resultMessage.getBody()).thenReturn("test"); - when(expectedMessage.getBody()).thenReturn("test"); + @ParameterizedTest + @ValueSource( + strings = {"namespaceRelabeled.xml", "fieldsReordered.xml", "attributes_reordered.xml"}) + void When_twoXMLisComparedWithSameContentInDifferentFormats(String fileName) + throws IOException { + String xmlExample = readFile("test data/xml/original.xml"); + when(actual.getMessage().getBody()).thenReturn(xmlExample); - ValidationResult validationResult = bodyValidatorSubject.execute(result, expected); + String xmlExampleWithChangedNamespacePrefixes = readFile("test data/xml/" + fileName); + when(expected.getMessage().getBody()).thenReturn(xmlExampleWithChangedNamespacePrefixes); - assertThat(validationResult).isEqualTo(VALIDATION_RESULT_SUCCESSFUL); - } + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); - @Test - void When_execute_With_EmptyActualAndExpected_Then_Success() { - Message resultMessage = mock(Message.class); - Message expectedMessage = mock(Message.class); - when(result.getMessage()).thenReturn(resultMessage); - when(expected.getMessage()).thenReturn(expectedMessage); - when(resultMessage.getBody()).thenReturn(""); - when(expectedMessage.getBody()).thenReturn(""); + assertThat(validationResult).isEqualTo(VALIDATION_RESULT_SUCCESSFUL); + } - ValidationResult validationResult = bodyValidatorSubject.execute(result, expected); + @Test + void When_twoXMLStringsDifferInWhitespacesBetweenTags() throws IOException { + String xmlExample = readFile("test data/xml/original.xml"); + when(actual.getMessage().getBody()).thenReturn(xmlExample); + when(expected.getMessage().getBody()).thenReturn(xmlExample.replace("", " ")); - assertThat(validationResult).isEqualTo(VALIDATION_RESULT_SUCCESSFUL); - } + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); + + assertThat(validationResult).isEqualTo(VALIDATION_RESULT_SUCCESSFUL); + } + + @Test + void When_twoJsonStringsDifferInWhitespacesBetweenQuotes() { + String jsonExample = NAME_JOHN_AGE_30_CAR_NULL_EXAMPLE; + when(actual.getMessage().getBody()).thenReturn(jsonExample); + when(expected.getMessage().getBody()).thenReturn(jsonExample.replace(",", " , ")); + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); - @Test - void When_isApplicable_Expect_Success() { - when(expected.getMessage().getBody()).thenReturn(RESULT); + assertThat(validationResult).isEqualTo(VALIDATION_RESULT_SUCCESSFUL); + } - boolean isApplicable = bodyValidatorSubject.isApplicable(result, expected); + @Test + void When_twoJsonStringsDifferInFieldsOrder() { + String jsonExampleReordered = "{\"car\":null,\"age\":30, \"name\":\"John\"}"; + when(actual.getMessage().getBody()).thenReturn(NAME_JOHN_AGE_30_CAR_NULL_EXAMPLE); + when(expected.getMessage().getBody()).thenReturn(jsonExampleReordered); + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); - assertThat(isApplicable).isTrue(); + assertThat(validationResult).isEqualTo(VALIDATION_RESULT_SUCCESSFUL); + } } - @Test - void When_isApplicable_With_MissingBody_Then_Fail() { - when(expected.getMessage().getBody()).thenReturn(null); + @Nested + class ValidationFails { + @Test + void When_actualAndExpectedAreDifferentStrings() { + when(actual.getMessage().getBody()).thenReturn("some content"); + when(expected.getMessage().getBody()).thenReturn("some other content"); + + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); + + assertThat(validationResult).isEqualTo(VALIDATION_RESULT_UNSUCCESSFUL); + } - boolean isApplicable = bodyValidatorSubject.isApplicable(result, expected); + @Test + void When_expectedHasValueAndActualIsNull() { + when(actual.getMessage().getBody()).thenReturn(null); + when(expected.getMessage().getBody()).thenReturn("test"); + + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); + + assertThat(validationResult).isEqualTo(VALIDATION_RESULT_UNSUCCESSFUL); + } + + @Test + void When_ActualAndExpectedAreDifferentXMLs_Then_describedDifferenceIsReturned() + throws IOException { + String xmlExample = readFile("test data/xml/original.xml"); + when(actual.getMessage().getBody()).thenReturn(xmlExample); + when(expected.getMessage().getBody()).thenReturn(xmlExample.replace(":table>", ":mable>")); + + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); + + assertThat(validationResult.isSuccess()).isFalse(); + AssertionsForClassTypes.assertThat(validationResult.getMessage()) + .isNotBlank() + .contains("mable"); + } + + @Test + void + When_ActualAndExpectedAreDifferentJSONsThatMismatchesInLetterCase_Then_describedDifferenceIsReturned() { + String jsonExample = NAME_JOHN_AGE_30_CAR_NULL_EXAMPLE; + when(actual.getMessage().getBody()).thenReturn(jsonExample); + when(expected.getMessage().getBody()).thenReturn(jsonExample.replace("car", "CAR")); + + ValidationResult validationResult = bodyValidatorSubject.execute(actual, expected); + + assertThat(validationResult.isSuccess()).isFalse(); + assertThat(validationResult.getMessage()) + .contains("not equal: only on left={CAR=null}: only on right={car=null}"); + } + } + + private static String parseNull(String actualValue) { + return "null".equals(actualValue) ? null : actualValue; + } - assertThat(isApplicable).isFalse(); + private String readFile(String path) throws IOException { + return FileUtils.readFileToString( + new ClassPathResource(path).getFile(), StandardCharsets.UTF_8); } } diff --git a/sip-test-kit/src/test/resources/logback-test.xml b/sip-test-kit/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..62ddc6ed6a --- /dev/null +++ b/sip-test-kit/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/sip-test-kit/src/test/resources/test data/xml/attributes_reordered.xml b/sip-test-kit/src/test/resources/test data/xml/attributes_reordered.xml new file mode 100644 index 0000000000..d9c0892921 --- /dev/null +++ b/sip-test-kit/src/test/resources/test data/xml/attributes_reordered.xml @@ -0,0 +1,17 @@ + + + + + Apples + Bananas + + + + + African Coffee Table + 80 + 120 + + + \ No newline at end of file diff --git a/sip-test-kit/src/test/resources/test data/xml/fieldsReordered.xml b/sip-test-kit/src/test/resources/test data/xml/fieldsReordered.xml new file mode 100644 index 0000000000..063ff48d6d --- /dev/null +++ b/sip-test-kit/src/test/resources/test data/xml/fieldsReordered.xml @@ -0,0 +1,15 @@ + + + African Coffee Table + 80 + 120 + + + + + Apples + Bananas + + + \ No newline at end of file diff --git a/sip-test-kit/src/test/resources/test data/xml/namespaceRelabeled.xml b/sip-test-kit/src/test/resources/test data/xml/namespaceRelabeled.xml new file mode 100644 index 0000000000..1fcbaaf57e --- /dev/null +++ b/sip-test-kit/src/test/resources/test data/xml/namespaceRelabeled.xml @@ -0,0 +1,17 @@ + + + + + Apples + Bananas + + + + + African Coffee Table + 80 + 120 + + + \ No newline at end of file diff --git a/sip-test-kit/src/test/resources/test data/xml/original.xml b/sip-test-kit/src/test/resources/test data/xml/original.xml new file mode 100644 index 0000000000..6ab0bb7748 --- /dev/null +++ b/sip-test-kit/src/test/resources/test data/xml/original.xml @@ -0,0 +1,17 @@ + + + + + Apples + Bananas + + + + + African Coffee Table + 80 + 120 + + + \ No newline at end of file