From 089325cd5100100e76f0ab9a2ba4eed7359fa3c2 Mon Sep 17 00:00:00 2001 From: dev-mlb <19797865+dev-mlb@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:25:59 -0400 Subject: [PATCH 1/2] extraction-test :: move answer checking to IBaseDataObjectDiffHelper --- .../emissary/core/DiffCheckConfiguration.java | 118 ++++- .../core/DiffCheckConfigurationTest.java | 122 +++++ .../core/IBaseDataObjectDiffHelper.java | 492 +++++++++++++++++- .../core/IBaseDataObjectDiffHelperTest.java | 346 +++++++++++- .../test/core/junit5/ExtractionTest.java | 447 +--------------- .../test/core/junit5/TestExtractionTest.java | 132 +---- .../emissary/util/PlaceComparisonHelper.java | 78 ++- .../util/PlaceComparisonHelperTest.java | 68 +++ 8 files changed, 1213 insertions(+), 590 deletions(-) diff --git a/src/test/java/emissary/core/DiffCheckConfiguration.java b/src/test/java/emissary/core/DiffCheckConfiguration.java index 546794ec69..572904bf32 100644 --- a/src/test/java/emissary/core/DiffCheckConfiguration.java +++ b/src/test/java/emissary/core/DiffCheckConfiguration.java @@ -1,5 +1,8 @@ package emissary.core; +import jakarta.annotation.Nullable; +import org.jdom2.Element; + import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; @@ -21,6 +24,16 @@ public enum DiffCheckOptions { */ private final Set enabled; + /** + * Flag indicating if the verification is in strict mode. + */ + private final boolean strict; + + /** + * The JDOM element containing expected values for lenient checks. + */ + private final Element lenientExpectationElement; + /** * Start building a new configuration * @@ -36,7 +49,25 @@ public static DiffCheckConfiguration.DiffCheckBuilder configure() { * @return a new config instance which only enables checking data */ public static DiffCheckConfiguration onlyCheckData() { - return new DiffCheckConfiguration(EnumSet.of(DiffCheckOptions.DATA)); + return new DiffCheckConfiguration(EnumSet.of(DiffCheckOptions.DATA), true, null); + } + + /** + * Check if the configuration is in strict mode + * + * @return if strict mode is enabled + */ + public boolean isStrict() { + return this.strict; + } + + /** + * Accessor for the lenient expectation XML element + * + * @return the lenient expectation element + */ + public Element getLenientExpectationElement() { + return this.lenientExpectationElement; } /** @@ -107,8 +138,21 @@ public Set getEnabled() { * * @param enabled set of pre-configured options */ - private DiffCheckConfiguration(final EnumSet enabled) { + private DiffCheckConfiguration(final EnumSet enabled, final boolean strict, + @Nullable final Element lenientExpectationElement) { this.enabled = Collections.unmodifiableSet(enabled); + this.strict = strict; + this.lenientExpectationElement = lenientExpectationElement; + } + + /** + * Creates a builder pre-populated with the settings of an existing configuration. + * + * @param prototype The configuration instance to copy from. + * @return A DiffCheckBuilder primed with the prototype's settings. + */ + public static DiffCheckBuilder from(final DiffCheckConfiguration prototype) { + return new DiffCheckBuilder(prototype); } /** @@ -121,13 +165,48 @@ public static class DiffCheckBuilder { */ private final EnumSet building; + /** + * Internal strict state tracking + */ + private boolean strict = true; + + /** + * Internal element tracking for lenient modes + */ + @Nullable + private Element lenientExpectationElement = null; + + /** + * Set strict processing mode behavior + * + * @param strict true to execute exact-match tracking, false for lenient checks + * @return the builder + */ + public DiffCheckBuilder setStrict(boolean strict) { + this.strict = strict; + return this; + } + + /** + * Set the underlying template element to evaluate lenient parameter rules + * + * @param element the source XML mapping element + * @return the builder + */ + public DiffCheckBuilder setLenientExpectationElement(Element element) { + this.lenientExpectationElement = element; + /* Setting a lenient expectation element implies lenient mode */ + this.strict = false; + return this; + } + /** * Finish building and create the final DiffCheckConfiguration object * * @return a new Configuration instance with the enabled options */ public DiffCheckConfiguration build() { - return new DiffCheckConfiguration(building); + return new DiffCheckConfiguration(building, strict, lenientExpectationElement); } /** @@ -154,6 +233,37 @@ private DiffCheckBuilder() { building = EnumSet.noneOf(DiffCheckOptions.class); } + /** + * Public/Package-private constructor to build from an existing configuration + * + * @param configuration the configuration to duplicate state from + */ + private DiffCheckBuilder(final DiffCheckConfiguration configuration) { + this.building = EnumSet.noneOf(DiffCheckOptions.class); + + if (configuration.checkData()) { + this.building.add(DiffCheckOptions.DATA); + } + if (configuration.checkTimestamp()) { + this.building.add(DiffCheckOptions.TIMESTAMP); + } + if (configuration.checkInternalId()) { + this.building.add(DiffCheckOptions.INTERNAL_ID); + } + if (configuration.checkTransformHistory()) { + this.building.add(DiffCheckOptions.TRANSFORM_HISTORY); + } + if (configuration.performDetailedParameterDiff()) { + this.building.add(DiffCheckOptions.DETAILED_PARAMETER_DIFF); + } + if (configuration.performKeyValueParameterDiff()) { + this.building.add(DiffCheckOptions.KEY_VALUE_PARAMETER_DIFF); + } + + this.strict = configuration.isStrict(); + this.lenientExpectationElement = configuration.getLenientExpectationElement(); + } + /** * Reset the list of enabled options * @@ -161,6 +271,8 @@ private DiffCheckBuilder() { */ public DiffCheckBuilder reset() { building.clear(); + this.strict = true; + this.lenientExpectationElement = null; return this; } diff --git a/src/test/java/emissary/core/DiffCheckConfigurationTest.java b/src/test/java/emissary/core/DiffCheckConfigurationTest.java index 2ad9a39a88..88d19023fb 100644 --- a/src/test/java/emissary/core/DiffCheckConfigurationTest.java +++ b/src/test/java/emissary/core/DiffCheckConfigurationTest.java @@ -131,4 +131,126 @@ void checkExplicit() { DiffCheckOptions.DETAILED_PARAMETER_DIFF, DiffCheckOptions.KEY_VALUE_PARAMETER_DIFF)); } + + @Test + void testFromConfiguration() { + final DiffCheckBuilder originalBuilder = DiffCheckConfiguration.configure(); + originalBuilder.enableData(); + originalBuilder.enableTimestamp(); + originalBuilder.enableDetailedParameterDiff(); + final DiffCheckConfiguration originalConfig = originalBuilder.build(); + + final DiffCheckBuilder copiedBuilder = DiffCheckConfiguration.from(originalConfig); + final DiffCheckConfiguration copiedConfig = copiedBuilder.build(); + + assertTrue(copiedConfig.checkData()); + assertTrue(copiedConfig.checkTimestamp()); + assertFalse(copiedConfig.checkInternalId()); + assertFalse(copiedConfig.checkTransformHistory()); + assertTrue(copiedConfig.performDetailedParameterDiff()); + assertFalse(copiedConfig.performKeyValueParameterDiff()); + } + + @Test + void testStrictMode() { + final DiffCheckBuilder builder = DiffCheckConfiguration.configure(); + + // Default is strict + DiffCheckConfiguration config = builder.build(); + assertTrue(config.isStrict()); + + // Disable strict mode + builder.setStrict(false); + config = builder.build(); + assertFalse(config.isStrict()); + + // Re-enable strict mode + builder.setStrict(true); + config = builder.build(); + assertTrue(config.isStrict()); + } + + @Test + void testOnlyCheckDataFactory() { + final DiffCheckConfiguration config = DiffCheckConfiguration.onlyCheckData(); + + assertTrue(config.checkData()); + assertFalse(config.checkTimestamp()); + assertFalse(config.checkInternalId()); + assertFalse(config.checkTransformHistory()); + assertFalse(config.performDetailedParameterDiff()); + assertFalse(config.performKeyValueParameterDiff()); + assertEquals(1, config.getEnabled().size()); + } + + @Test + void testMultipleBuilderModifications() { + final DiffCheckBuilder builder = DiffCheckConfiguration.configure(); + + builder.enableData(); + builder.enableTimestamp(); + builder.enableInternalId(); + builder.enableTransformHistory(); + + final DiffCheckConfiguration config1 = builder.build(); + assertTrue(config1.checkData()); + assertTrue(config1.checkTimestamp()); + assertTrue(config1.checkInternalId()); + assertTrue(config1.checkTransformHistory()); + assertEquals(4, config1.getEnabled().size()); + + // Disable some options + builder.disableTimestamp(); + builder.disableInternalId(); + + final DiffCheckConfiguration config2 = builder.build(); + assertTrue(config2.checkData()); + assertFalse(config2.checkTimestamp()); + assertFalse(config2.checkInternalId()); + assertTrue(config2.checkTransformHistory()); + assertEquals(2, config2.getEnabled().size()); + } + + @Test + void testMutualExclusivityOfParameterDiffs() { + final DiffCheckBuilder builder = DiffCheckConfiguration.configure(); + + builder.enableDetailedParameterDiff(); + DiffCheckConfiguration config = builder.build(); + assertTrue(config.performDetailedParameterDiff()); + assertFalse(config.performKeyValueParameterDiff()); + + // Enabling key-value should disable detailed + builder.enableKeyValueParameterDiff(); + config = builder.build(); + assertFalse(config.performDetailedParameterDiff()); + assertTrue(config.performKeyValueParameterDiff()); + + // Enabling detailed should disable key-value + builder.enableDetailedParameterDiff(); + config = builder.build(); + assertTrue(config.performDetailedParameterDiff()); + assertFalse(config.performKeyValueParameterDiff()); + } + + @Test + void testCumulativeEnablingAllOptions() { + final DiffCheckBuilder builder = DiffCheckConfiguration.configure(); + + builder.enableData(); + builder.enableTimestamp(); + builder.enableInternalId(); + builder.enableTransformHistory(); + builder.enableDetailedParameterDiff(); + + final DiffCheckConfiguration config = builder.build(); + + assertTrue(config.checkData()); + assertTrue(config.checkTimestamp()); + assertTrue(config.checkInternalId()); + assertTrue(config.checkTransformHistory()); + assertTrue(config.performDetailedParameterDiff()); + assertFalse(config.performKeyValueParameterDiff()); + assertEquals(5, config.getEnabled().size()); + } } diff --git a/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java b/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java index 732f8c9c5e..3b74551e45 100644 --- a/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java +++ b/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java @@ -3,35 +3,68 @@ import emissary.core.channels.SeekableByteChannelFactory; import emissary.core.constants.IbdoXmlElementNames; +import jakarta.annotation.Nullable; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.math.NumberUtils; +import org.jdom2.Attribute; +import org.jdom2.Element; import java.io.IOException; import java.io.InputStream; import java.nio.channels.Channels; import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.stream.Collectors; +import static emissary.core.IBaseDataObjectXmlCodecs.BASE64; +import static emissary.core.IBaseDataObjectXmlCodecs.BASE64_DECODER; +import static emissary.core.IBaseDataObjectXmlCodecs.ENCODING_ATTRIBUTE_NAME; +import static emissary.core.constants.IbdoXmlElementNames.ATTACHMENT_ELEMENT_PREFIX; import static emissary.core.constants.IbdoXmlElementNames.BROKEN; import static emissary.core.constants.IbdoXmlElementNames.CLASSIFICATION; +import static emissary.core.constants.IbdoXmlElementNames.CURRENT_FORM; +import static emissary.core.constants.IbdoXmlElementNames.DATA; +import static emissary.core.constants.IbdoXmlElementNames.EXTRACTED_RECORD_ELEMENT_PREFIX; +import static emissary.core.constants.IbdoXmlElementNames.EXTRACT_COUNT; +import static emissary.core.constants.IbdoXmlElementNames.FILE_TYPE; +import static emissary.core.constants.IbdoXmlElementNames.FONT_ENCODING; +import static emissary.core.constants.IbdoXmlElementNames.INDEX; +import static emissary.core.constants.IbdoXmlElementNames.NAME; +import static emissary.core.constants.IbdoXmlElementNames.NOMETA; +import static emissary.core.constants.IbdoXmlElementNames.NOVIEW; +import static emissary.core.constants.IbdoXmlElementNames.NUM_ATTACHMENTS; import static emissary.core.constants.IbdoXmlElementNames.PARAMETER; +import static emissary.core.constants.IbdoXmlElementNames.SHORT_NAME; +import static emissary.core.constants.IbdoXmlElementNames.VALUE; +import static emissary.core.constants.IbdoXmlElementNames.VIEW; public class IBaseDataObjectDiffHelper { + + // Environment/OS isolation verification targets + private static final String SYSTEM_OS_RELEASE = System.getProperty("os.name", "").toLowerCase(Locale.getDefault()); + private static final String MAJOR_OS_VERSION = System.getProperty("os.version", ""); + + // Centralized message template definitions private static final String DIFF_NOT_NULL_MSG = "Required: differences not null"; private static final String ID_NOT_NULL_MSG = "Required: identifier not null"; private static final String ARE_NOT_EQUAL = " are not equal"; - private static final String SHORT_NAME = "shortName"; private static final String INTERNAL_ID = "internalId"; private static final String TRANSFORM_HISTORY = "transformHistory"; private static final String FILE_TYPE_EMPTY = "fileTypeEmpty"; @@ -40,6 +73,14 @@ public class IBaseDataObjectDiffHelper { private IBaseDataObjectDiffHelper() {} + /** + * This method compares two IBaseDataObject's and adds any differences to the provided string list. + * + * @param expected the first IBaseDataObject to compare. + * @param actual the second IBaseDataObject to compare. + * @param differences the string list differences are to be added to. + * @param options {@link DiffCheckConfiguration} containing config to specify whether to check data etc. + */ /** * This method compares two IBaseDataObject's and adds any differences to the provided string list. * @@ -53,6 +94,8 @@ public static void diff(final IBaseDataObject expected, final IBaseDataObject ac Validate.notNull(expected, "Required: \"expected\" ibdo not null"); Validate.notNull(actual, "Required: \"actual\" ibdo not null"); Validate.notNull(differences, DIFF_NOT_NULL_MSG); + Validate.notNull(options, "Required: options not null"); + Validate.isTrue(options.isStrict(), "Cannot invoke strict multi-payload diff() when configuration is set to lenient mode."); if (options.checkData()) { final SeekableByteChannelFactory sbcf1 = expected.getChannelFactory(); @@ -106,6 +149,46 @@ public static void diff(final IBaseDataObject expected, final IBaseDataObject ac diff(expected.getExtractedRecords(), actual.getExtractedRecords(), EXTRACTED_RECORDS, differences, options); } + /** + * This method compares an IBaseDataObject and against an answer file adds any differences to the provided string list. + * + * @param actual the IBaseDataObject to compare. + * @param differences the string list of differences are to be added to. + * @param options {@link DiffCheckConfiguration} containing config to specify whether to check data etc. + */ + public static void diff(final IBaseDataObject actual, final List differences, final DiffCheckConfiguration options) { + Validate.notNull(actual, "Required: \"actual\" ibdo not null"); + Validate.notNull(differences, DIFF_NOT_NULL_MSG); + Validate.notNull(options, "Required: options not null"); + Validate.isTrue(!options.isStrict(), "Cannot invoke single-payload lenient diff() when configuration is set to strict mode."); + + Element expectationElement = options.getLenientExpectationElement(); + if (expectationElement != null) { + checkCurrentForms(expectationElement, actual, differences); + checkFields(expectationElement, actual, differences); + checkMetadata(expectationElement, actual, differences); + checkViews(expectationElement, actual, differences); + checkExtractedRecords(expectationElement, actual, differences, options); + } + } + + /** + * This method compares an IBaseDataObject and against an answer file adds any differences to the provided string list. + * + * @param actualIbdo the IBaseDataObject to compare. + * @param actualChildren the list of child IBaseDataObjects to compare. + * @param identifier a string that helps identify the context of comparing these two list of IBaseDataObjects. + * @param parentDifferences the string list of parent differences are to be added to. + * @param childDifferences the string list of child differences are to be added to. + * @param options {@link DiffCheckConfiguration} containing config to specify whether to check data etc. + */ + public static void diff(final IBaseDataObject actualIbdo, final List actualChildren, final String identifier, + final List parentDifferences, final List childDifferences, final DiffCheckConfiguration options) { + checkAttachmentCounts(options.getLenientExpectationElement(), actualChildren, parentDifferences); + diff(actualIbdo, parentDifferences, options); + diff(actualChildren, identifier, childDifferences, options); + } + /** * This method compares two lists of IBaseDataObject's and adds any differences to the provided string list. * @@ -119,6 +202,8 @@ public static void diff(final List expected, final List differences, final DiffCheckConfiguration options) { Validate.notNull(identifier, ID_NOT_NULL_MSG); Validate.notNull(differences, DIFF_NOT_NULL_MSG); + Validate.notNull(options, "Required: options not null"); + Validate.isTrue(options.isStrict(), "Cannot invoke strict multi-list diff() when configuration is set to lenient mode."); final int expectedSize = (expected == null) ? 0 : expected.size(); final int actualSize = (actual == null) ? 0 : actual.size(); @@ -140,6 +225,50 @@ public static void diff(final List expected, final List actual, final String identifier, + final List differences, final DiffCheckConfiguration options) { + Validate.notNull(identifier, ID_NOT_NULL_MSG); + Validate.notNull(differences, DIFF_NOT_NULL_MSG); + Validate.notNull(options, "Required: options not null"); + Validate.isTrue(!options.isStrict(), "Cannot invoke single-list lenient diff() when configuration is set to strict mode."); + + Element rootXml = options.getLenientExpectationElement(); + if (rootXml != null && actual != null) { + final List childDifferences = new ArrayList<>(); + for (int i = 0; i < actual.size(); i++) { + childDifferences.clear(); + + Element attel = getChildAnswers(rootXml, ATTACHMENT_ELEMENT_PREFIX, i + 1, differences); + if (attel != null) { + + diff(attel, actual.get(i), childDifferences, options); + + final String prefix = String.format("%s[index %d] : ", identifier, i); + for (String diff : childDifferences) { + differences.add(prefix + diff); + } + } + } + } + } + + protected static void diff(Element attel, final IBaseDataObject actual, final List differences, final DiffCheckConfiguration options) { + DiffCheckConfiguration childOpts = DiffCheckConfiguration.from(options) + .setLenientExpectationElement(attel) + .build(); + + checkAttachmentCounts(attel, null, differences); + diff(actual, differences, childOpts); + } + /** * This method compares two {@link SeekableByteChannelFactory} (SBCF) objects and adds any differences to the provided * string list. @@ -345,4 +474,365 @@ public static Map> convertMap(final Map differences) { + Element el = parent.getChildren().stream() + .filter(c -> c.getName().equalsIgnoreCase(name) && String.valueOf(index).equals(c.getAttributeValue(INDEX)) + && verifyOs(c, differences)) + .findFirst() + .orElse(null); + if (el == null) { + el = parent.getChildren().stream() + .filter(c -> c.getName().equalsIgnoreCase(name + index) && verifyOs(c, differences)) + .findFirst() + .orElse(null); + } + return el; + } + + protected static void checkAttachmentCounts(Element el, @Nullable List attachments, List differences) { + int payloadSize = attachments != null ? attachments.size() : 0; + Element numAttEl = null; + long numAttElements = 0; + + for (Element child : el.getChildren()) { + if (verifyOs(child, differences)) { + String name = child.getName(); + if (name.equals(NUM_ATTACHMENTS) && numAttEl == null) { + numAttEl = child; + } else if (name.startsWith(ATTACHMENT_ELEMENT_PREFIX)) { + numAttElements++; + } + } + } + + if (numAttEl != null) { + int numAtt = NumberUtils.toInt(numAttEl.getValue(), -1); + if (numAtt != payloadSize) { + differences + .add(String.format("Expected %d not equal to number of attachments in payload (%d).", numAtt, payloadSize)); + } + } else if (numAttElements > 0) { + if (numAttElements != payloadSize) { + differences.add( + String.format("Expected count %d not equal to number of attachments in payload (%d).", numAttElements, payloadSize)); + } + } else if (payloadSize > 0) { + differences.add(String.format("%d attachments in payload with no count in answer xml. Add matching ", payloadSize)); + } + } + + protected static void checkCurrentForms(Element el, IBaseDataObject payload, List differences) { + for (Element currentForm : el.getChildren(CURRENT_FORM)) { + if (verifyOs(currentForm, differences)) { + String cf = currentForm.getTextTrim(); + if (cf != null) { + Attribute index = currentForm.getAttribute(INDEX); + if (index != null) { + try { + int idxVal = index.getIntValue(); + String actualForm = payload.currentFormAt(idxVal); + if (!cf.equals(actualForm)) { + differences.add(String.format("Current form '%s' not found at position [%d]. Actual: %s, All: %s", cf, idxVal, + actualForm, payload.getAllCurrentForms())); + } + } catch (Exception e) { + differences.add("Invalid index integer value parsing currentForm rule: " + index.getValue()); + } + } else if (payload.searchCurrentForm(cf) <= -1) { + differences.add(String.format("Current form '%s' not found in payload forms: %s", cf, payload.getAllCurrentForms())); + } + } + } + } + } + + protected static void checkFields(Element el, IBaseDataObject payload, List differences) { + for (Element child : el.getChildren(FILE_TYPE)) { + if (verifyOs(child, differences)) { + diff(child.getTextTrim(), payload.getFileType(), "Expected File Type", differences); + } + } + for (Element child : el.getChildren(CLASSIFICATION)) { + if (verifyOs(child, differences)) { + diff(child.getTextTrim(), payload.getClassification(), "Classification mismatch", differences); + } + } + for (Element child : el.getChildren(SHORT_NAME)) { + if (verifyOs(child, differences)) { + diff(child.getTextTrim(), payload.shortName(), "Shortname", differences); + } + } + for (Element child : el.getChildren(FONT_ENCODING)) { + if (verifyOs(child, differences)) { + diff(child.getTextTrim(), payload.getFontEncoding(), "Font encoding", differences); + } + } + for (Element child : el.getChildren(BROKEN)) { + if (verifyOs(child, differences)) { + diff(child.getTextTrim(), Boolean.toString(payload.isBroken()), "Broken status", differences); + } + } + + for (Element child : el.getChildren("currentFormSize")) { + if (verifyOs(child, differences)) { + diff(NumberUtils.toInt(child.getValue(), -1), payload.currentFormSize(), "Current form size", differences); + } + } + for (Element child : el.getChildren("dataLength")) { + if (verifyOs(child, differences)) { + diff(NumberUtils.toInt(child.getValue(), -1), payload.dataLength(), "Data length", differences); + } + } + + for (Element child : el.getChildren("procError")) { + if (verifyOs(child, differences)) { + String expectedError = child.getTextTrim(); + if (StringUtils.isNotBlank(expectedError)) { + String actualError = payload.getProcessingError(); + if (actualError == null) { + differences.add(String.format("Expected processing error '%s', but got null", expectedError)); + } else { + String normalizedActual = actualError.replace("\n", ";"); + diff(expectedError, normalizedActual, "Processing Error mismatch", differences); + } + } + } + } + } + + protected static void checkMetadata(Element el, IBaseDataObject payload, List differences) { + for (Element meta : el.getChildren(PARAMETER)) { + if (verifyOs(meta, differences)) { + String key = meta.getChildTextTrim(NAME); + if (key == null) { + differences.add(String.format("The element %s missing a child name element", PARAMETER)); + } else { + checkStringValue(meta, payload.getStringParameter(key), differences); + } + } + } + + for (Element meta : el.getChildren(NOMETA)) { + if (verifyOs(meta, differences)) { + String key = meta.getChildTextTrim(NAME); + if (key == null) { + differences.add(String.format("The element %s missing a child name element", NOMETA)); + } else if (payload.hasParameter(key)) { + differences.add( + String.format("Metadata element '%s' should not exist, but has value of '%s'", key, payload.getStringParameter(key))); + } + } + } + } + + protected static void checkViews(Element el, IBaseDataObject payload, List differences) { + List dataElements = el.getChildren(DATA); + if (!dataElements.isEmpty()) { + String primaryDataStr = payload.data() != null ? new String(payload.data(), StandardCharsets.UTF_8) : ""; + for (Element dataEl : dataElements) { + if (verifyOs(dataEl, differences)) { + int length = NumberUtils.toInt(dataEl.getChildTextTrim("length"), -1); + if (length > -1 && length != payload.dataLength()) { + differences.add(String.format("Data length mismatch -> Expected: %d, Actual: %d", length, payload.dataLength())); + } + checkStringValue(dataEl, primaryDataStr, differences); + } + } + } + + for (Element view : el.getChildren(VIEW)) { + if (verifyOs(view, differences)) { + String viewName = view.getChildTextTrim(NAME); + byte[] viewData = payload.getAlternateView(viewName); + if (viewData == null) { + differences.add(String.format("Alternate View '%s' is missing from payload", viewName)); + } else { + String lengthStr = view.getChildTextTrim("length"); + if (lengthStr != null && Integer.parseInt(lengthStr) != viewData.length) { + differences.add(String.format("Length of Alternate View '%s' mismatch -> Expected: %s, Actual: %d", viewName, lengthStr, + viewData.length)); + } + checkStringValue(view, new String(viewData, StandardCharsets.UTF_8), differences); + } + } + } + + for (Element view : el.getChildren(NOVIEW)) { + if (verifyOs(view, differences)) { + String viewName = view.getChildTextTrim(NAME); + if (payload.getAlternateView(viewName) != null) { + differences.add(String.format("Alternate View '%s' is present, but was flagged as forbidden (noview)", viewName)); + } + } + } + } + + protected static void checkExtractedRecords(Element el, IBaseDataObject payload, List differences, final DiffCheckConfiguration options) { + List extractedChildren = payload.hasExtractedRecords() + ? Objects.requireNonNullElse(payload.getExtractedRecords(), List.of()) + : List.of(); + int payloadSize = extractedChildren.size(); + + List validChildren = el.getChildren().stream() + .filter(child -> verifyOs(child, differences)) + .collect(Collectors.toList()); + + Optional extractCountEl = validChildren.stream() + .filter(child -> EXTRACT_COUNT.equals(child.getName())) + .findFirst(); + + long numExtractElements = validChildren.stream() + .filter(child -> child.getName().startsWith(EXTRACTED_RECORD_ELEMENT_PREFIX)) + .count(); + + validateExtractCounts(extractCountEl.orElse(null), numExtractElements, payloadSize, differences); + + for (int i = 0; i < payloadSize; i++) { + int extNum = i + 1; + Element extel = getChildAnswers(el, EXTRACTED_RECORD_ELEMENT_PREFIX, extNum, differences); + if (extel == null) { + continue; + } + + List childDiffs = new ArrayList<>(); + diff(extel, extractedChildren.get(i), childDiffs, options); + + String prefix = String.format("extract%d :: ", extNum); + childDiffs.stream() + .map(diff -> prefix + diff) + .forEach(differences::add); + } + } + + protected static void validateExtractCounts(@Nullable Element extractCountEl, long numExtractElements, int payloadSize, + List differences) { + int extractCount = extractCountEl != null ? NumberUtils.toInt(extractCountEl.getValue(), -1) : -1; + if (extractCount > -1) { + if (extractCount != payloadSize) { + differences + .add(String.format("Expected %d not equal to number of extracts in payload (%d).", extractCount, payloadSize)); + } + } else if (numExtractElements > 0) { + if (numExtractElements != payloadSize) { + differences.add(String.format("Expected count %d not equal to number of extracts in payload (%d).", numExtractElements, + payloadSize)); + } + } else if (payloadSize > 0) { + differences.add(String.format("%d extracts in payload with no count in answer xml. Add matching ", payloadSize)); + } + } + + protected static void checkStringValue(Element meta, String data, List differences) { + Element valueElement = meta.getChild(VALUE); + String value = (valueElement != null) ? valueElement.getText() : null; + if (value == null || "null".equalsIgnoreCase(value)) { + return; + } + + String encoding = valueElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); + String matchMode = (encoding != null && !encoding.isEmpty()) + ? encoding + : meta.getAttributeValue("matchMode", "equals"); + String key = meta.getChildTextTrim(NAME); + + String truncatedData = truncate(data); + + switch (matchMode.toLowerCase(Locale.getDefault())) { + case "equals": + if (!Objects.equals(value, data)) { + differences.add(formatErr(meta, key, "does not equal", truncatedData, truncate(value))); + } + break; + case "index": + case "contains": + if (data == null || !data.contains(value)) { + differences.add(formatErr(meta, key, "does not contain", truncatedData, truncate(value))); + } + break; + case "!index": + case "!contains": + if (data != null && data.contains(value)) { + differences.add(formatErr(meta, key, "should not contain", truncatedData, truncate(value))); + } + break; + case "match": + if (data == null || !data.matches(value)) { + differences.add(formatErr(meta, key, "does not match regex", truncatedData, truncate(value))); + } + break; + case BASE64: + try { + value = new String(BASE64_DECODER.decode(value)); + if (!Objects.equals(value, data)) { + differences.add(formatErr(meta, key, "Base64 mismatch", truncatedData, truncate(value))); + } + } catch (RuntimeException e) { + differences.add(String.format("%s element '%s': Base64 decoding failed.", meta.getName(), key)); + } + break; + case "collection": + handleCollectionMatch(meta, data, value, key, differences); + break; + default: + differences.add(String.format("Problematic matchMode '%s' for test '%s' in %s", matchMode, key, meta.getName())); + break; + } + } + + protected static void handleCollectionMatch(Element meta, String data, String value, String key, List differences) { + Attribute sepAttr = meta.getAttribute("collectionSeparator"); + String separator = (sepAttr != null) ? sepAttr.getValue() : ","; + + List expectedValues = Arrays.asList((value != null ? value : "").split(separator)); + List actualValues = Arrays.asList((data != null ? data : "").split(separator)); + + if (!CollectionUtils.isEqualCollection(expectedValues, actualValues)) { + differences.add(String.format("%s element '%s': collections not equal. Sep: '%s' | Expected: %s, Actual: %s", + meta.getName(), key, separator, expectedValues, actualValues)); + } + } + + protected static boolean verifyOs(Element element, List differences) { + Attribute specifiedOs = element.getAttribute("os-release"); + Attribute specifiedVersion = element.getAttribute("os-version"); + + if (specifiedOs == null) { + // No OS restriction, safe to run + return true; + } + + String os = specifiedOs.getValue().toLowerCase(Locale.getDefault()); + switch (os) { + case "ubuntu": + case "centos": + case "rhel": + case "mac": + if (specifiedVersion != null) { + return os.equalsIgnoreCase(SYSTEM_OS_RELEASE) && specifiedVersion.getValue().equals(MAJOR_OS_VERSION); + } else { + return os.equalsIgnoreCase(SYSTEM_OS_RELEASE); + } + default: + differences.add(String.format("Unsupported or mistyped os-release target '%s' found in element <%s>", + specifiedOs.getValue(), element.getName())); + return false; + } + } + + protected static String formatErr(Element meta, String key, String reason, String actual, String expected) { + return String.format("%s element '%s' problem: '%s' %s '%s'", meta.getName(), key, actual, reason, expected); + } + + protected static String truncate(String input) { + return truncate(input, 150); + } + + protected static String truncate(String input, int maxLength) { + if (input == null || input.length() <= maxLength) { + return input; + } + return input.substring(0, maxLength); + } + } diff --git a/src/test/java/emissary/core/IBaseDataObjectDiffHelperTest.java b/src/test/java/emissary/core/IBaseDataObjectDiffHelperTest.java index 2b0d18d475..6380eaa976 100644 --- a/src/test/java/emissary/core/IBaseDataObjectDiffHelperTest.java +++ b/src/test/java/emissary/core/IBaseDataObjectDiffHelperTest.java @@ -3,9 +3,12 @@ import emissary.core.channels.AbstractSeekableByteChannel; import emissary.core.channels.InMemoryChannelFactory; import emissary.core.channels.SeekableByteChannelFactory; +import emissary.core.constants.IbdoXmlElementNames; import emissary.test.core.junit5.UnitTest; import jakarta.annotation.Nullable; +import org.jdom2.Attribute; +import org.jdom2.Element; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -17,6 +20,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -25,9 +29,19 @@ import java.util.TreeMap; import java.util.TreeSet; +import static emissary.core.IBaseDataObjectDiffHelper.checkAttachmentCounts; +import static emissary.core.IBaseDataObjectDiffHelper.checkMetadata; +import static emissary.core.IBaseDataObjectDiffHelper.checkStringValue; +import static emissary.core.IBaseDataObjectDiffHelper.checkViews; +import static emissary.core.IBaseDataObjectDiffHelper.diff; +import static emissary.core.IBaseDataObjectDiffHelper.verifyOs; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class IBaseDataObjectDiffHelperTest extends UnitTest { @@ -35,7 +49,10 @@ class IBaseDataObjectDiffHelperTest extends UnitTest { private IBaseDataObject ibdo2; private List ibdoList1; private List ibdoList2; + private IBaseDataObject mockPayload; + private DiffCheckConfiguration mockOptions; private List differences; + private Element mockElement; private static final DiffCheckConfiguration EMPTY_OPTIONS = DiffCheckConfiguration.configure().build(); private static final DiffCheckConfiguration CHECK_DATA = DiffCheckConfiguration.onlyCheckData(); @@ -45,7 +62,10 @@ void setup() { ibdo2 = new BaseDataObject(); ibdoList1 = Arrays.asList(ibdo1); ibdoList2 = Arrays.asList(ibdo2); + mockPayload = mock(IBaseDataObject.class); + mockOptions = mock(DiffCheckConfiguration.class); differences = new ArrayList<>(); + mockElement = mock(Element.class); } private void verifyDiff(final List expectedDifferencesForward, final List expectedDifferencesReverse) { @@ -54,13 +74,13 @@ private void verifyDiff(final List expectedDifferencesForward, final Lis private void verifyDiff(final List expectedDifferencesForward, final List expectedDifferencesReverse, final DiffCheckConfiguration options) { - IBaseDataObjectDiffHelper.diff(ibdo1, ibdo1, differences, options); + diff(ibdo1, ibdo1, differences, options); assertEquals(0, differences.size()); - IBaseDataObjectDiffHelper.diff(ibdo1, ibdo2, differences, options); + diff(ibdo1, ibdo2, differences, options); replaceVariableText(differences); assertIterableEquals(expectedDifferencesForward, differences); differences.clear(); - IBaseDataObjectDiffHelper.diff(ibdo2, ibdo1, differences, options); + diff(ibdo2, ibdo1, differences, options); replaceVariableText(differences); assertIterableEquals(expectedDifferencesReverse, differences); differences.clear(); @@ -80,9 +100,9 @@ private static void replaceVariableText(final List differences) { private void verifyDiffList(final List expectedDifferences, @Nullable final List list1, @Nullable final List list2) { - IBaseDataObjectDiffHelper.diff(list1, list1, "test", differences, EMPTY_OPTIONS); + diff(list1, list1, "test", differences, EMPTY_OPTIONS); assertEquals(0, differences.size()); - IBaseDataObjectDiffHelper.diff(list1, list2, "test", differences, EMPTY_OPTIONS); + diff(list1, list2, "test", differences, EMPTY_OPTIONS); assertIterableEquals(expectedDifferences, differences); differences.clear(); } @@ -94,28 +114,28 @@ private static void checkThrowsNull(final Executable e) { @Test void testDiffArguments() { // Objects - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(null, ibdo2, differences, EMPTY_OPTIONS)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(ibdo1, null, differences, EMPTY_OPTIONS)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(ibdo1, ibdo2, null, EMPTY_OPTIONS)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(ibdoList1, ibdoList2, null, differences, EMPTY_OPTIONS)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(ibdoList1, ibdoList2, "id", null, EMPTY_OPTIONS)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(ibdo1, ibdo2, differences, null)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(new Object(), new Object(), null, differences)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(new Object(), new Object(), "id", null)); + checkThrowsNull(() -> diff((IBaseDataObject) null, ibdo2, differences, EMPTY_OPTIONS)); + checkThrowsNull(() -> diff(ibdo1, null, differences, EMPTY_OPTIONS)); + checkThrowsNull(() -> diff(ibdo1, ibdo2, null, EMPTY_OPTIONS)); + checkThrowsNull(() -> diff(ibdoList1, ibdoList2, null, differences, EMPTY_OPTIONS)); + checkThrowsNull(() -> diff(ibdoList1, ibdoList2, "id", null, EMPTY_OPTIONS)); + checkThrowsNull(() -> diff(ibdo1, ibdo2, differences, null)); + checkThrowsNull(() -> diff(new Object(), new Object(), null, differences)); + checkThrowsNull(() -> diff(new Object(), new Object(), "id", null)); // Integers - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(0, 0, null, differences)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(0, 0, "id", null)); + checkThrowsNull(() -> diff(0, 0, null, differences)); + checkThrowsNull(() -> diff(0, 0, "id", null)); // Booleans - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(false, false, null, differences)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(false, false, "id", null)); + checkThrowsNull(() -> diff(false, false, null, differences)); + checkThrowsNull(() -> diff(false, false, "id", null)); // Maps - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(null, new HashMap<>(), "id", differences)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(new HashMap<>(), null, "id", differences)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(new HashMap<>(), new HashMap<>(), null, differences)); - checkThrowsNull(() -> IBaseDataObjectDiffHelper.diff(new HashMap<>(), new HashMap<>(), "id", null)); + checkThrowsNull(() -> diff(null, new HashMap<>(), "id", differences)); + checkThrowsNull(() -> diff(new HashMap<>(), null, "id", differences)); + checkThrowsNull(() -> diff(new HashMap<>(), new HashMap<>(), null, differences)); + checkThrowsNull(() -> diff(new HashMap<>(), new HashMap<>(), "id", null)); } @Test @@ -406,4 +426,288 @@ void testParamSort() { TreeMap> sortedParams = new TreeMap<>(ibdo1.getParameters()); assertEquals(expectedParams, sortedParams.keySet(), "parameters should be sorted in natural order of keys"); } + + @Test + void testCheckAttachmentCounts() { + Element root = new Element("answers"); + root.addContent(new Element(IbdoXmlElementNames.NUM_ATTACHMENTS).setText("2")); + + List attachments = new ArrayList<>(); + attachments.add(new BaseDataObject()); + + List diffs = new ArrayList<>(); + checkAttachmentCounts(root, attachments, diffs); + + assertEquals(1, diffs.size()); + assertEquals("Expected 2 not equal to number of attachments in payload (1).", diffs.get(0)); + } + + @Test + void testLenientDiffListAttachmentCountMismatch() { + Element root = new Element("answers"); + root.addContent(new Element(IbdoXmlElementNames.NUM_ATTACHMENTS).setText("3")); + + List actual = new ArrayList<>(); + actual.add(new BaseDataObject()); + + DiffCheckConfiguration options = DiffCheckConfiguration.configure().setLenientExpectationElement(root).build(); + + List diffs = new ArrayList<>(); + diff(new BaseDataObject(), actual, "test", diffs, diffs, options); + + assertEquals(1, diffs.size()); + assertEquals("Expected 3 not equal to number of attachments in payload (1).", diffs.get(0)); + } + + + @Test + void shouldThrowExceptionWhenStrictModeIsEnabled() { + when(mockOptions.isStrict()).thenReturn(true); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> diff(mockPayload, differences, mockOptions) + ); + + assertTrue(exception.getMessage().contains("Cannot invoke single-payload lenient diff()")); + } + + @Test + void shouldDoNothingIfRootXmlIsNull() { + when(mockOptions.isStrict()).thenReturn(false); + when(mockOptions.getLenientExpectationElement()).thenReturn(null); + + diff(mockPayload, differences, mockOptions); + + assertTrue(differences.isEmpty()); + } + + @Test + void shouldReportMismatchWhenNumAttachmentsDiffers() { + Element root = new Element("root"); + Element numAtt = new Element("numAttachments").setText("5"); + root.addContent(numAtt); + + List attachments = List.of(mockPayload, mockPayload); // Size = 2 + + checkAttachmentCounts(root, attachments, differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("Expected 5 not equal to number of attachments in payload (2).")); + } + + @Test + void shouldReportMissingCountTagWhenPayloadHasItems() { + Element root = new Element("root"); + List attachments = List.of(mockPayload); + + checkAttachmentCounts(root, attachments, differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("1 attachments in payload with no count in answer xml")); + } + + @Test + void shouldReportErrorIfParameterNameMissing() { + Element root = new Element("answers"); + Element param = new Element("meta"); // No name element added + root.addContent(param); + + checkMetadata(root, mockPayload, differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("missing a child name element")); + } + + @Test + void shouldReportErrorIfForbiddenMetadataExists() { + Element root = new Element("root"); + Element nometa = new Element("nometa"); + nometa.addContent(new Element("name").setText("forbiddenKey")); + root.addContent(nometa); + + when(mockPayload.hasParameter("forbiddenKey")).thenReturn(true); + when(mockPayload.getStringParameter("forbiddenKey")).thenReturn("someValue"); + + checkMetadata(root, mockPayload, differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("Metadata element 'forbiddenKey' should not exist, but has value of 'someValue'")); + } + + @Test + void shouldReportMissingView() { + Element root = new Element("root"); + Element view = new Element("view"); + view.addContent(new Element("name").setText("TEXT_VIEW")); + root.addContent(view); + + when(mockPayload.getAlternateView("TEXT_VIEW")).thenReturn(null); + + checkViews(root, mockPayload, differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("Alternate View 'TEXT_VIEW' is missing from payload")); + } + + @Test + void shouldReportForbiddenViewPresence() { + Element root = new Element("root"); + Element noview = new Element("noview"); + noview.addContent(new Element("name").setText("SECRET_VIEW")); + root.addContent(noview); + + when(mockPayload.getAlternateView("SECRET_VIEW")).thenReturn("secret data".getBytes(StandardCharsets.UTF_8)); + + checkViews(root, mockPayload, differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("Alternate View 'SECRET_VIEW' is present, but was flagged as forbidden (noview)")); + } + + @Test + void testEqualsMatchSuccess() { + Element meta = buildMetaElement("equals", "hello"); + checkStringValue(meta, "hello", differences); + + assertTrue(differences.isEmpty()); + } + + @Test + void testEqualsMatchFailure() { + Element meta = buildMetaElement("equals", "hello"); + checkStringValue(meta, "world", differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("does not equal")); + } + + @Test + void testContainsMatchSuccess() { + Element meta = buildMetaElement("contains", "bar"); + checkStringValue(meta, "foo bar baz", differences); + + assertTrue(differences.isEmpty()); + } + + @Test + void testRegexMatchFailure() { + Element meta = buildMetaElement("match", "^[0-9]+$"); + checkStringValue(meta, "abc1234", differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("does not match regex")); + } + + @Test + void testBase64MatchSuccess() { + String originalStr = "DecodedText123"; + String encoded = Base64.getEncoder().encodeToString(originalStr.getBytes(StandardCharsets.UTF_8)); + + Element meta = buildMetaElement("base64", encoded); + checkStringValue(meta, originalStr, differences); + + assertTrue(differences.isEmpty()); + } + + @Test + void testBase64MatchFailure() { + Element meta = buildMetaElement("base64", "!!!not-valid-base64!!!"); + checkStringValue(meta, "anything", differences); + + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("Base64 mismatch")); + } + + @Test + void testCollectionMatch() { + Element meta = buildMetaElement("collection", "A|B|C"); + meta.setAttribute("collectionSeparator", "\\|"); + + // Equal ignoring order + checkStringValue(meta, "C|A|B", differences); + assertTrue(differences.isEmpty()); + + // Missing element triggers error + checkStringValue(meta, "A|B", differences); + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("collections not equal")); + } + + @Test + void shouldReturnTrueWhenNoOsAttribute() { + Element element = new Element("field"); + boolean result = verifyOs(element, differences); + + assertTrue(result); + assertTrue(differences.isEmpty()); + } + + @Test + void shouldFailAndRecordErrorForUnsupportedOs() { + Element element = new Element("field"); + element.setAttribute("os-release", "windows"); + + boolean result = verifyOs(element, differences); + + assertFalse(result); + assertEquals(1, differences.size()); + assertTrue(differences.get(0).contains("Unsupported or mistyped os-release target 'windows'")); + } + + private static Element buildMetaElement(String mode, String valueStr) { + Element meta = new Element("parameter"); + meta.addContent(new Element("name").setText("testKey")); + if (mode != null) { + meta.setAttribute("matchMode", mode); + } + if (valueStr != null) { + Element valEl = new Element("value").setText(valueStr); + meta.addContent(valEl); + } + return meta; + } + + @Test + void testVerifyOs_NoOsRestriction_ReturnsTrue() { + when(mockElement.getAttribute("os-release")).thenReturn(null); + + boolean result = verifyOs(mockElement, differences); + + assertTrue(result, "Should return true if no OS is specified"); + assertTrue(differences.isEmpty(), "Differences list should remain empty"); + } + + @Test + void testVerifyOs_SupportedOsMatchesButVersionMismatches_ReturnsFalse() { + Attribute osAttr = mock(Attribute.class); + when(osAttr.getValue()).thenReturn("ubuntu"); + + Attribute versionAttr = mock(Attribute.class); + when(versionAttr.getValue()).thenReturn("20.04"); + + when(mockElement.getAttribute("os-release")).thenReturn(osAttr); + when(mockElement.getAttribute("os-version")).thenReturn(versionAttr); + + boolean result = verifyOs(mockElement, differences); + + assertFalse(result, "Should return false when OS matches but version is wrong"); + assertTrue(differences.isEmpty()); + } + + @Test + void testVerifyOs_UnsupportedOs_ReturnsFalseAndAddsDifference() { + Attribute osAttr = mock(Attribute.class); + when(osAttr.getValue()).thenReturn("windows"); + when(mockElement.getAttribute("os-release")).thenReturn(osAttr); + when(mockElement.getName()).thenReturn("test-node"); + + boolean result = verifyOs(mockElement, differences); + + assertFalse(result, "Should return false for unsupported OS"); + assertEquals(1, differences.size(), "Should log exactly one difference error"); + + String expectedError = "Unsupported or mistyped os-release target 'windows' found in element "; + assertEquals(expectedError, differences.get(0)); + } } diff --git a/src/test/java/emissary/test/core/junit5/ExtractionTest.java b/src/test/java/emissary/test/core/junit5/ExtractionTest.java index dd623b3ed8..95f06f56ac 100644 --- a/src/test/java/emissary/test/core/junit5/ExtractionTest.java +++ b/src/test/java/emissary/test/core/junit5/ExtractionTest.java @@ -2,7 +2,6 @@ import emissary.core.DataObjectFactory; import emissary.core.DiffCheckConfiguration; -import emissary.core.Family; import emissary.core.IBaseDataObject; import emissary.core.IBaseDataObjectDiffHelper; import emissary.core.IBaseDataObjectXmlCodecs; @@ -19,13 +18,10 @@ import com.google.errorprone.annotations.ForOverride; import jakarta.annotation.Nullable; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.jdom2.Attribute; import org.jdom2.DataConversionException; import org.jdom2.Document; import org.jdom2.Element; @@ -44,61 +40,31 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Stream; import static emissary.core.IBaseDataObjectXmlCodecs.ALT_DEFAULT_ELEMENT_ENCODERS; import static emissary.core.IBaseDataObjectXmlCodecs.ALWAYS_SHA256_ELEMENT_ENCODERS; -import static emissary.core.IBaseDataObjectXmlCodecs.BASE64; -import static emissary.core.IBaseDataObjectXmlCodecs.BASE64_DECODER; import static emissary.core.IBaseDataObjectXmlCodecs.DEFAULT_ELEMENT_DECODERS; -import static emissary.core.IBaseDataObjectXmlCodecs.ENCODING_ATTRIBUTE_NAME; -import static emissary.core.IBaseDataObjectXmlCodecs.LENGTH_ATTRIBUTE_NAME; import static emissary.core.IBaseDataObjectXmlCodecs.SHA256_ELEMENT_ENCODERS; import static emissary.core.constants.IbdoXmlElementNames.ANSWERS; -import static emissary.core.constants.IbdoXmlElementNames.ATTACHMENT_ELEMENT_PREFIX; import static emissary.core.constants.IbdoXmlElementNames.BAD_ALT_VIEW; -import static emissary.core.constants.IbdoXmlElementNames.BROKEN; -import static emissary.core.constants.IbdoXmlElementNames.CLASSIFICATION; -import static emissary.core.constants.IbdoXmlElementNames.CURRENT_FORM; -import static emissary.core.constants.IbdoXmlElementNames.DATA; import static emissary.core.constants.IbdoXmlElementNames.DATA_FILE; -import static emissary.core.constants.IbdoXmlElementNames.EXTRACTED_RECORD_ELEMENT_PREFIX; -import static emissary.core.constants.IbdoXmlElementNames.EXTRACT_COUNT; import static emissary.core.constants.IbdoXmlElementNames.FILE_TYPE; -import static emissary.core.constants.IbdoXmlElementNames.FONT_ENCODING; -import static emissary.core.constants.IbdoXmlElementNames.INDEX; import static emissary.core.constants.IbdoXmlElementNames.INITIAL_FORM; import static emissary.core.constants.IbdoXmlElementNames.INPUT_ALT_VIEW; -import static emissary.core.constants.IbdoXmlElementNames.NAME; -import static emissary.core.constants.IbdoXmlElementNames.NOMETA; -import static emissary.core.constants.IbdoXmlElementNames.NOVIEW; -import static emissary.core.constants.IbdoXmlElementNames.NUM_ATTACHMENTS; -import static emissary.core.constants.IbdoXmlElementNames.PARAMETER; import static emissary.core.constants.IbdoXmlElementNames.SETUP; -import static emissary.core.constants.IbdoXmlElementNames.SHORT_NAME; -import static emissary.core.constants.IbdoXmlElementNames.VALUE; -import static emissary.core.constants.IbdoXmlElementNames.VIEW; import static emissary.test.core.junit5.AnswerGenerator.fixDisposeRunnables; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public abstract class ExtractionTest extends UnitTest { protected static final Logger logger = LoggerFactory.getLogger(ExtractionTest.class); - private static final List NO_ATTACHMENTS = Collections.emptyList(); private static final byte[] INCORRECT_VIEW_MESSAGE = "This is the incorrect view, the place should not have processed this view".getBytes(); private volatile AnswerGenerator answerGenerator; @@ -122,9 +88,9 @@ public abstract class ExtractionTest extends UnitTest { @Nullable protected IServiceProviderPlace place = null; @Nullable - private static final String SYSTEM_OS_RELEASE; + public static final String SYSTEM_OS_RELEASE; @Nullable - private static final String MAJOR_OS_VERSION; + public static final String MAJOR_OS_VERSION; static { if (OSReleaseUtil.isUbuntu()) { @@ -219,7 +185,7 @@ public void setGenerateAnswerFiles(String generateAnswerFiles) { * @return diff helper configuration */ public DiffCheckConfiguration getDiffCheck() { - return DiffCheckConfiguration.configure().enableData().enableKeyValueParameterDiff().build(); + return DiffCheckConfiguration.configure().enableData().enableKeyValueParameterDiff().setStrict(isStrict()).build(); } @Override @@ -597,9 +563,9 @@ protected void checkAnswersPostHook(Element answers, IBaseDataObject payload, IB protected void checkAnswers(Document answers, IBaseDataObject payload, List attachments, String tname) throws DataConversionException { + assertNotNull(place); Element root = answers.getRootElement(); Element parent = root.getChild(ANSWERS); - if (isStrict()) { assertNotNull(parent, "No 'answers' section found!"); checkAnswersStrict(answers, payload, attachments, tname, parent); @@ -608,16 +574,6 @@ protected void checkAnswers(Document answers, IBaseDataObject payload, List attachments, String tname) - throws DataConversionException { - checkAttachmentCounts(el, attachments, tname); - checkCurrentForms(el, payload, tname); - checkFields(el, payload, tname); - checkMetadata(el, payload, tname); - checkViews(el, payload, tname); - checkExtractedRecords(el, payload, tname); - } - /** * LENIENT: Recursive element-based check for payload and attachments. * @@ -627,24 +583,17 @@ protected void checkAnswers(Element el, IBaseDataObject payload, @Nullable List< * @param tname the path of the dat file * @throws DataConversionException data conversion from a string to value type fails */ - protected void checkAnswersLenient(Element parent, IBaseDataObject payload, - List attachments, String tname) throws DataConversionException { - // Check the main payload - checkAnswers(parent, payload, attachments, tname); - - // Check each attachment individually - for (int attNum = 1; attNum <= attachments.size(); attNum++) { - String atname = tname + Family.SEP + attNum; - Element el = getChildAnswers(parent, ATTACHMENT_ELEMENT_PREFIX, attNum); - - if (el != null) { - IBaseDataObject currentAtt = attachments.get(attNum - 1); - - checkAnswersPreHook(el, payload, currentAtt, atname); - checkAnswers(el, currentAtt, null, atname); - checkAnswersPostHook(el, payload, currentAtt, atname); - } - } + protected void checkAnswersLenient(Element parent, IBaseDataObject payload, List attachments, String tname) + throws DataConversionException { + + DiffCheckConfiguration diffOptions = DiffCheckConfiguration.from(getDiffCheck()) + .setLenientExpectationElement(parent) + .setStrict(isStrict()) + .build(); + + List actualAttachments = attachments != null ? attachments : new ArrayList<>(); + final String differences = PlaceComparisonHelper.checkDifferences(payload, actualAttachments, place.getClass().getName(), diffOptions); + assertDifferences(differences); } /** @@ -656,366 +605,35 @@ protected void checkAnswersLenient(Element parent, IBaseDataObject payload, * @param tname the path of the dat file * @param parent XML element for the parent */ - protected void checkAnswersStrict(Document answers, IBaseDataObject payload, - List attachments, String tname, Element parent) { + protected void checkAnswersStrict(Document answers, IBaseDataObject payload, List attachments, String tname, Element parent) { final List expectedAttachments = new ArrayList<>(); final IBaseDataObject expectedIbdo = IBaseDataObjectXmlHelper.ibdoFromXml(answers, expectedAttachments, getDecoders(tname)); - assertNotNull(place); final String differences = PlaceComparisonHelper.checkDifferences( expectedIbdo, payload, expectedAttachments, attachments, place.getClass().getName(), getDiffCheck()); - String message = generateAnswers() - ? differences + "\nNOTE: Since 'generateAnswers' is true, these differences could indicate non-deterministic processing\n" - : differences; - - assertNull(differences, message); + assertDifferences(differences); // Strict mode also validates Log Events assertIterableEquals(SimplifiedLogEvent.fromXml(parent), actualSimplifiedLogEvents); } - protected void checkAttachmentCounts(Element el, List attachments, String tname) { - int payloadSize = attachments != null ? attachments.size() : 0; - - Element numAttEl = null; - long numAttElements = 0; + protected void assertDifferences(final String differences) { + if (differences != null) { + StringBuilder failureMessage = new StringBuilder(differences); - for (Element child : el.getChildren()) { - if (verifyOs(child)) { - String name = child.getName(); - if (name.equals(NUM_ATTACHMENTS) && numAttEl == null) { - numAttEl = child; - } else if (name.startsWith(ATTACHMENT_ELEMENT_PREFIX)) { - numAttElements++; - } + if (generateAnswers()) { + failureMessage + .append("\nNOTE: Since 'generateAnswers' is true, ") + .append("these differences could indicate non-deterministic processing"); } - } - if (numAttEl != null) { - int numAtt = Integer.parseInt(numAttEl.getValue()); - assertEquals(numAtt, payloadSize, - String.format(Locale.getDefault(), "Expected in %s not equal to number of att in payload.", tname)); - } else if (numAttElements > 0) { - assertEquals(numAttElements, payloadSize, - String.format(Locale.getDefault(), "Expected in %s not equal to number of att in payload.", tname)); - } else if (payloadSize > 0) { - fail(String.format(Locale.getDefault(), - "%d attachments in payload with no count in answer xml, add matching count for %s", - payloadSize, tname)); + fail(failureMessage.toString()); } } - protected void checkCurrentForms(Element el, IBaseDataObject payload, String tname) throws DataConversionException { - for (Element currentForm : el.getChildren(CURRENT_FORM)) { - if (verifyOs(currentForm)) { - String cf = currentForm.getTextTrim(); - if (cf != null) { - Attribute index = currentForm.getAttribute(INDEX); - if (index != null) { - assertEquals(payload.currentFormAt(index.getIntValue()), cf, - String.format(Locale.getDefault(), "Current form '%s' not found at position [%d] in %s, %s", - cf, index.getIntValue(), tname, payload.getAllCurrentForms())); - } else { - assertTrue(payload.searchCurrentForm(cf) > -1, - String.format(Locale.getDefault(), "Current form '%s' not found in %s, %s", cf, tname, payload.getAllCurrentForms())); - } - } - } - } - } - - protected void checkFields(Element el, IBaseDataObject payload, String tname) { - // File Type - assertStringProperty(el, FILE_TYPE, payload.getFileType(), - ft -> String.format(Locale.getDefault(), "Expected File Type '%s' in %s", ft, tname)); - - // Current Form Size - assertIntProperty(el, "currentFormSize", payload.currentFormSize(), - size -> String.format(Locale.getDefault(), "Current form size '%d' does not match in %s", size, tname)); - - // Classification - assertStringProperty(el, CLASSIFICATION, payload.getClassification(), - cl -> String.format(Locale.getDefault(), "Classification in '%s' is '%s', not expected '%s'", tname, payload.getClassification(), - cl)); - - // Data Length - assertIntProperty(el, "dataLength", payload.dataLength(), - len -> String.format(Locale.getDefault(), "Data length '%d' does not match in %s", len, tname)); - - // Short Name - assertStringProperty(el, SHORT_NAME, payload.shortName(), val -> "Shortname does not match expected in " + tname); - - // Font Encoding - assertStringProperty(el, FONT_ENCODING, payload.getFontEncoding(), val -> "Font encoding does not match expected in " + tname); - - // Broken - assertStringProperty(el, BROKEN, Boolean.toString(payload.isBroken()), val -> "Broken status in " + tname); - - // Processing Error - assertProcessingError(el, payload.getProcessingError(), expected -> String.format("Expected processing error '%s' in %s", expected, tname), - () -> "Processing Error does not match expected in " + tname); - } - - protected void checkMetadata(Element el, IBaseDataObject payload, String tname) { - // Parameters - for (Element meta : el.getChildren(PARAMETER)) { - if (verifyOs(meta)) { - String key = meta.getChildTextTrim(NAME); - checkForMissingNameElement(PARAMETER, key, tname); - checkStringValue(meta, payload.getStringParameter(key), tname); - } - } - - // Nometa - for (Element meta : el.getChildren(NOMETA)) { - if (verifyOs(meta)) { - String key = meta.getChildTextTrim(NAME); - checkForMissingNameElement(NOMETA, key, tname); - assertFalse(payload.hasParameter(key), - String.format(Locale.getDefault(), "Metadata element '%s' in '%s' should not exist, but has value of '%s'", key, tname, - payload.getStringParameter(key))); - } - } - } - - protected void checkViews(Element el, IBaseDataObject payload, String tname) { - // Primary Data View - List dataElements = el.getChildren(DATA); - if (!dataElements.isEmpty()) { - String primaryDataStr = new String(payload.data()); // Allocated once safely out of loop - for (Element dataEl : dataElements) { - if (verifyOs(dataEl)) { - int length = NumberUtils.toInt(dataEl.getChildTextTrim(LENGTH_ATTRIBUTE_NAME), -1); - if (length > -1) { - assertEquals(length, payload.dataLength(), "Data length in " + tname); - } - checkStringValue(dataEl, primaryDataStr, tname); - } - } - } - - // Alternate Views - for (Element view : el.getChildren(VIEW)) { - if (verifyOs(view)) { - String viewName = view.getChildTextTrim(NAME); - byte[] viewData = payload.getAlternateView(viewName); - assertNotNull(viewData, String.format(Locale.getDefault(), "Alternate View '%s' is missing in %s", viewName, tname)); - - String lengthStr = view.getChildTextTrim(LENGTH_ATTRIBUTE_NAME); - if (lengthStr != null) { - assertEquals(Integer.parseInt(lengthStr), viewData.length, - String.format(Locale.getDefault(), "Length of Alternate View '%s' is wrong in %s", viewName, tname)); - } - checkStringValue(view, new String(viewData), tname); - } - } - - // Noview - for (Element view : el.getChildren(NOVIEW)) { - if (verifyOs(view)) { - String viewName = view.getChildTextTrim(NAME); - assertNull(payload.getAlternateView(viewName), - String.format(Locale.getDefault(), "Alternate View '%s' is present, but should not be, in %s", viewName, tname)); - } - } - } - - protected void checkExtractedRecords(Element el, IBaseDataObject payload, String tname) throws DataConversionException { - List extractedChildren = payload.hasExtractedRecords() ? payload.getExtractedRecords() : List.of(); - int payloadSize = extractedChildren.size(); - - Element extractCountEl = null; - long numExtractElements = 0; - - for (Element child : el.getChildren()) { - if (verifyOs(child)) { - String name = child.getName(); - if (name.equals(EXTRACT_COUNT) && extractCountEl == null) { - extractCountEl = child; - } else if (name.startsWith(EXTRACTED_RECORD_ELEMENT_PREFIX)) { - numExtractElements++; - } - } - } - - int extractCount = extractCountEl != null ? Integer.parseInt(extractCountEl.getValue()) : -1; - if (extractCount > -1) { - assertEquals(extractCount, payloadSize, - String.format(Locale.getDefault(), "Expected in %s not equal to number of extracts in payload.", tname)); - } else if (numExtractElements > 0) { - assertEquals(numExtractElements, payloadSize, - String.format(Locale.getDefault(), "Expected in %s not equal to number of extracts in payload.", tname)); - } else if (payloadSize > 0) { - fail(String.format(Locale.getDefault(), - "%d extracts in payload with no count in answer xml, add matching count for %s", payloadSize, tname)); - } - - for (int attNum = 1; attNum <= payloadSize; attNum++) { - Element extel = getChildAnswers(el, EXTRACTED_RECORD_ELEMENT_PREFIX, attNum); - if (extel != null) { - checkAnswers(extel, extractedChildren.get(attNum - 1), NO_ATTACHMENTS, - String.format(Locale.getDefault(), "%s::extract%d", tname, attNum)); - } - } - } - - protected void assertStringProperty(Element parent, String childName, String actualValue, Function messageProvider) { - for (Element child : parent.getChildren(childName)) { - if (verifyOs(child)) { - String expectedValue = child.getTextTrim(); - if (StringUtils.isNotBlank(expectedValue)) { - assertEquals(expectedValue, actualValue, messageProvider.apply(expectedValue)); - } - } - } - } - - protected void assertIntProperty(Element parent, String childName, int actualValue, Function messageProvider) { - for (Element child : parent.getChildren(childName)) { - if (verifyOs(child)) { - int expectedValue; - try { - expectedValue = Integer.parseInt(child.getValue()); - } catch (NumberFormatException e) { - expectedValue = -1; - } - if (expectedValue > -1) { - assertEquals(expectedValue, actualValue, messageProvider.apply(expectedValue)); - } - } - } - } - - protected void assertProcessingError(Element parent, String actualError, Function notNullMessageProvider, - Supplier equalityMessageProvider) { - for (Element child : parent.getChildren("procError")) { - if (verifyOs(child)) { - String expectedError = child.getTextTrim(); - if (StringUtils.isNotBlank(expectedError)) { - assertNotNull(actualError, notNullMessageProvider.apply(expectedError)); - String normalizedActual = actualError.replace("\n", ";"); - assertEquals(expectedError, normalizedActual, equalityMessageProvider.get()); - } - } - } - } - - private static void checkForMissingNameElement(String parentTag, String key, String tname) { - if (key == null) { - fail(String.format(Locale.getDefault(), "The element %s has a problem in %s: does not have a child name element", parentTag, tname)); - } - } - - protected void checkStringValue(Element meta, String data, String tname) { - Element valueElement = meta.getChild(VALUE); - String value = (valueElement != null) ? valueElement.getText() : null; - if (value == null || "null".equalsIgnoreCase(value)) { - return; - } - - // Determine matchMode: Priority to 'encoding' attribute, then 'matchMode', default to 'equals' - String encoding = valueElement.getAttributeValue(ENCODING_ATTRIBUTE_NAME); - String matchMode = (encoding != null && !encoding.isEmpty()) - ? encoding - : meta.getAttributeValue("matchMode", "equals"); - - String key = meta.getChildTextTrim(NAME); - - var truncatedData = truncate(data); - var truncatedValue = truncate(value); - - switch (matchMode) { - case "equals": - assertEquals(value, data, formatErr(meta, key, tname, "does not equal", truncatedData, truncatedValue)); - break; - - case INDEX: - case "contains": - assertTrue(data.contains(value), formatErr(meta, key, tname, "does not contain", truncatedData, truncatedValue)); - break; - - case "!index": - case "!contains": - assertFalse(data.contains(value), formatErr(meta, key, tname, "should not contain", truncatedData, truncatedValue)); - break; - - case "match": - assertTrue(data.matches(value), formatErr(meta, key, tname, "does not match regex", truncatedData, truncatedValue)); - break; - - case BASE64: - // decode value as a base64 encoded byte[] array and use the string - // representation of the byte array for comparison to the incoming value - value = new String(BASE64_DECODER.decode(value)); - truncatedValue = truncate(value); - assertEquals(value, data, formatErr(meta, key, tname, "Base64 mismatch", truncatedData, truncatedValue)); - break; - - default: - if ("collection".equalsIgnoreCase(matchMode)) { - handleCollectionMatch(meta, data, value, key, tname); - } else { - fail(String.format("Problematic matchMode '%s' for test '%s' in %s", matchMode, key, meta.getName())); - } - break; - } - } - - protected void handleCollectionMatch(Element meta, String data, String value, String key, String tname) { - Attribute sepAttr = meta.getAttribute("collectionSeparator"); - String separator = (sepAttr != null) ? sepAttr.getValue() : ","; - - List expectedValues = Arrays.asList(value.split(separator)); - List actualValues = Arrays.asList(data.split(separator)); - - String msg = String.format("%s element '%s' in %s: collections not equal. Sep: '%s'", - meta.getName(), key, tname, separator); - assertTrue(CollectionUtils.isEqualCollection(expectedValues, actualValues), msg); - } - - protected String formatErr(Element meta, String key, String tname, String reason, String actual, String expected) { - return String.format(Locale.getDefault(), "%s element '%s' problem in %s: '%s' %s '%s'", - meta.getName(), key, tname, actual, reason, expected); - } - - protected String truncate(String input) { - return truncate(input, 150); - } - - protected String truncate(String input, int maxLength) { - if (input == null || input.length() <= maxLength) { - return input; - } - return input.substring(0, maxLength); - } - - protected boolean verifyOs(Element element) { - Attribute specifiedOs = element.getAttribute("os-release"); - Attribute specifiedVersion = element.getAttribute("os-version"); - if (specifiedOs != null) { - String os = specifiedOs.getValue(); - switch (os) { - case "ubuntu": - case "centos": - case "rhel": - case "mac": - if (specifiedVersion != null) { - return os.equals(SYSTEM_OS_RELEASE) && specifiedVersion.getValue().equals(MAJOR_OS_VERSION); - } else { - // verify os matches, major os version not specified - return os.equals(SYSTEM_OS_RELEASE); - } - default: - fail("specified OS needs to match ubuntu, centos, rhel, or mac. Provided OS=" + os); - } - } - // os-release is not set as an attribute, element applicable for all os - return true; - } - protected void setupPayload(IBaseDataObject payload, Document doc) { if (!isStrict()) { kff.hash(payload); @@ -1061,19 +679,4 @@ protected void setupPayload(IBaseDataObject payload, Document doc) { payload.setFileType(payload.currentForm()); } } - - protected Element getChildAnswers(Element parent, String name, int index) { - // look up the new way i.e - Element el = parent.getChildren().stream() - .filter(c -> c.getName().equalsIgnoreCase(name) && c.getAttribute(INDEX).getValue().equals(String.valueOf(index)) && verifyOs(c)) - .findFirst() - .orElse(null); - if (el == null) { - // fallback to the old way i.e. - el = parent.getChildren().stream().filter(c -> c.getName().equalsIgnoreCase(name + index) && verifyOs(c)) - .findFirst() - .orElse(null); - } - return el; - } } diff --git a/src/test/java/emissary/test/core/junit5/TestExtractionTest.java b/src/test/java/emissary/test/core/junit5/TestExtractionTest.java index 5a661f9134..fea0741a34 100644 --- a/src/test/java/emissary/test/core/junit5/TestExtractionTest.java +++ b/src/test/java/emissary/test/core/junit5/TestExtractionTest.java @@ -2,10 +2,9 @@ import emissary.core.DataObjectFactory; import emissary.core.IBaseDataObject; -import emissary.util.os.OSReleaseUtil; +import emissary.place.IServiceProviderPlace; import org.jdom2.Document; -import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; import org.jdom2.input.sax.XMLReaders; @@ -13,14 +12,12 @@ import java.io.IOException; import java.io.InputStream; -import java.util.List; -import java.util.NoSuchElementException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class TestExtractionTest extends UnitTest { @@ -28,39 +25,6 @@ class TestExtractionTest extends UnitTest { private static final String RESOURCE_NAME = "/emissary/test/core/junit5/TestExtractionTest.xml"; private static final String MISSING_TAGS_RESOURCE = "/emissary/test/core/junit5/MissingTags.xml"; private static final String PROC_ERROR_RESOURCE = "/emissary/test/core/junit5/ProcessingErrorTest.xml"; - private static final String OS_SPECIFIC_RESOURCE = "/emissary/test/core/junit5/OsSpecificTest.xml"; - - @Test - void testCheckStringValueForCollection() throws JDOMException, IOException { - SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING); - InputStream inputStream = TestExtractionTest.class.getResourceAsStream(RESOURCE_NAME); - assertNotNull(inputStream, "Could not locate: " + RESOURCE_NAME); - Document answerDoc = builder.build(inputStream); - inputStream.close(); - - ExtractionTest test = spy(ExtractionTest.class); - - Element meta = answerDoc.getRootElement().getChild("answers").getChild("meta"); - test.checkStringValue(meta, "1;2;3;4;5;6;7", "testCheckStringValueForCollection"); - test.checkStringValue(meta, "1;3;4;2;5;6;7", "testCheckStringValueForCollection"); - test.checkStringValue(meta, "1;3;2;4;7;5;6", "testCheckStringValueForCollection"); - test.checkStringValue(meta, "7;6;5;4;3;2;1", "testCheckStringValueForCollection"); - } - - @Test - void testCheckStringValueForCollectionFailure() throws JDOMException, IOException { - SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING); - InputStream inputStream = TestExtractionTest.class.getResourceAsStream(RESOURCE_NAME); - assertNotNull(inputStream, "Could not locate: " + RESOURCE_NAME); - Document answerDoc = builder.build(inputStream); - inputStream.close(); - - ExtractionTest test = spy(ExtractionTest.class); - - Element meta = answerDoc.getRootElement().getChild("answers").getChild("meta"); - - assertThrows(AssertionError.class, () -> test.checkStringValue(meta, "7;0;0;0;2;1", "testCheckStringValueForCollection")); - } @Test void testCheckStringValueForFontEncoding() throws JDOMException, IOException { @@ -74,43 +38,6 @@ void testCheckStringValueForFontEncoding() throws JDOMException, IOException { assertEquals("UTF16", fontEncodingValue); } - @Test - void testCheckStringValueBangIndex() throws IOException, JDOMException { - SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING); - InputStream inputStream = TestExtractionTest.class.getResourceAsStream(RESOURCE_NAME); - assertNotNull(inputStream, "Could not locate: " + RESOURCE_NAME); - Document answerDoc = builder.build(inputStream); - inputStream.close(); - String matchMode = "!index"; - - ExtractionTest test = spy(ExtractionTest.class); - - List dataList = answerDoc.getRootElement().getChild("answers").getChildren("data"); - Element bangIndexData = getAttributeFromDataChild(dataList, matchMode); - - test.checkStringValue(bangIndexData, "time:20221229", "testCheckStringValue!IndexTrue"); - assertThrows(AssertionError.class, () -> test.checkStringValue(bangIndexData, "timestamp:20221229", "testCheckStringValue!IndexFalse")); - - matchMode = "!contains"; - - Element bangContainsData = getAttributeFromDataChild(dataList, matchMode); - - test.checkStringValue(bangContainsData, "time:20221229", "testCheckStringValue!ContainsTrue"); - assertThrows(AssertionError.class, () -> test.checkStringValue(bangContainsData, "timestamp:20221229", "testCheckStringValue!ContainsFalse")); - } - - private static Element getAttributeFromDataChild(List dataList, String matchMode) { - Element data = null; - // Having different matchModes in the same data necessitates having to go through each child and - // filter for the correct one - try { - data = dataList.stream().filter(item -> item.getAttribute("matchMode").getValue().equals(matchMode)).findFirst().get(); - } catch (NoSuchElementException e) { - fail("Attribute " + matchMode + " does not exist in data"); - } - return data; - } - @Test void testExtractionNoNameTags() throws JDOMException, IOException { IBaseDataObject d = DataObjectFactory.getInstance(); @@ -119,11 +46,10 @@ void testExtractionNoNameTags() throws JDOMException, IOException { SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING); InputStream inputStream = TestExtractionTest.class.getResourceAsStream(MISSING_TAGS_RESOURCE); Document doc = builder.build(inputStream); - Element answers = doc.getRootElement().getChild("answers"); // ensure that the test fails if the meta or nometa tags are not created correctly and a name tag is missing assertThrows(AssertionError.class, () -> { - test.checkAnswers(answers, d, null, MISSING_TAGS_RESOURCE); + test.checkAnswers(doc, d, null, MISSING_TAGS_RESOURCE); }, "The test should fail if we did not create the nometa tag correctly"); } @@ -137,61 +63,27 @@ void testProcessingErrorTag() throws IOException, JDOMException { inputStream.close(); ExtractionTest test = spy(ExtractionTest.class); + test.place = mock(IServiceProviderPlace.class); + // answer file is expecting 2 processing errors - Element answers = answerDoc.getRootElement().getChild("answers"); AssertionError error; // test with no processing error present in payload - error = assertThrows(AssertionError.class, () -> test.checkAnswers(answers, d, null, PROC_ERROR_RESOURCE), + error = assertThrows(AssertionError.class, () -> test.checkAnswers(answerDoc, d, null, PROC_ERROR_RESOURCE), "Test should fail, no processing errors added to payload."); - assertTrue(error.getMessage().endsWith("expected: not "), "Verify correct error message (assertNotNull)"); + assertTrue(error.getMessage().contains("but got null"), + "Verify correct error message (assertNotNull)"); // add first processing error to payload, verify fails at assertEquals d.addProcessingError("test processing error 1"); - error = assertThrows(AssertionError.class, () -> test.checkAnswers(answers, d, null, PROC_ERROR_RESOURCE), + error = assertThrows(AssertionError.class, () -> test.checkAnswers(answerDoc, d, null, PROC_ERROR_RESOURCE), "Test should fail, only one processing error added to payload."); - assertTrue(error.getMessage().endsWith("expected: but was: "), + assertTrue(error.getMessage().contains( + "Processing Error mismatch are not equal -> Expected: test processing error 1;test processing error 2;, Actual: test processing error 1;"), "Verify correct error message (assertEquals)"); // add second procError, should pass through checkAnswers now d.addProcessingError("test processing error 2"); - test.checkAnswers(answers, d, null, PROC_ERROR_RESOURCE); - } - - @Test - void testOsSpecificTest() throws IOException, JDOMException { - IBaseDataObject d = DataObjectFactory.getInstance(); - SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING); - InputStream inputStream = TestExtractionTest.class.getResourceAsStream(OS_SPECIFIC_RESOURCE); - assertNotNull(inputStream, "Could not locate: " + OS_SPECIFIC_RESOURCE); - Document answerDoc = builder.build(inputStream); - inputStream.close(); - - ExtractionTest test = spy(ExtractionTest.class); - Element answers = answerDoc.getRootElement().getChild("answers"); - - // done to test when no os specified - d.appendParameter("Test", "Test"); - - // append bdo data for os specific dataLength & data - if (OSReleaseUtil.isUbuntu()) { - d.setData("ubuntu-dataLength-test".getBytes()); - d.appendParameter("ubuntu-test", "ubuntu-test"); - } else if (OSReleaseUtil.isCentOs()) { - d.setData("centos-dataLength-test (make different than ubuntu)".getBytes()); - d.appendParameter("centos-test", "centos-test"); - } else if (OSReleaseUtil.isRhel()) { - d.setData("rhel-dataLength-test".getBytes()); - d.appendParameter("rhel-test", "rhel-test"); - } else if (OSReleaseUtil.isMac()) { - d.setData("mac-dataLength-test".getBytes()); - d.appendParameter("mac-test", "mac-test"); - } - - // verify if invalid os is specified, AssertionError is thrown - // with all things appended above, this should only fail at checking last element - AssertionError error = assertThrows(AssertionError.class, () -> test.checkAnswers(answers, d, null, OS_SPECIFIC_RESOURCE), - "Test should fail, last meta has a fake os."); - assertTrue(error.getMessage().endsWith("Provided OS=FAKE-OS")); + test.checkAnswers(answerDoc, d, null, PROC_ERROR_RESOURCE); } } diff --git a/src/test/java/emissary/util/PlaceComparisonHelper.java b/src/test/java/emissary/util/PlaceComparisonHelper.java index 63580b132c..936cf9838b 100644 --- a/src/test/java/emissary/util/PlaceComparisonHelper.java +++ b/src/test/java/emissary/util/PlaceComparisonHelper.java @@ -13,7 +13,10 @@ import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Helper class to use during development of a major refactoring or replacement of a Place. @@ -126,29 +129,58 @@ public static String checkDifferences(final IBaseDataObject expectedIbdo, final IBaseDataObjectDiffHelper.diff(expectedIbdo, actualIbdo, parentDifferences, options); IBaseDataObjectDiffHelper.diff(expectedChildren, actualChildren, identifier, childDifferences, options); - if (!parentDifferences.isEmpty() || !childDifferences.isEmpty()) { - final StringBuilder sb = new StringBuilder(); - for (int i = 0; i < parentDifferences.size(); i++) { - if (i != 0) { - sb.append(StringUtils.LF); - } - sb.append(identifier).append(": Parent Diff: "); - sb.append(parentDifferences.get(i)); - } - if (!parentDifferences.isEmpty() && !childDifferences.isEmpty()) { - sb.append(StringUtils.LF); - } - for (int i = 0; i < childDifferences.size(); i++) { - if (i != 0) { - sb.append(StringUtils.LF); - } - sb.append(identifier).append(": Child Diff: "); - sb.append(childDifferences.get(i)); - } - - return sb.toString(); - } + return buildDifferences(parentDifferences, childDifferences, identifier); + } - return null; + /** + * Given two BDOs and results from two processing place runs, compare them and log any differences. + * + * @param actualIbdo the 'main' BDO that was originally passed in from upstream + * @param actualChildren from the 'new' run with the 'actualIbdo' object + * @param identifier to highlight any differences in logs + * @param options {@link DiffCheckConfiguration} to configure diffing options + * @return the string of differences, or null if there aren't any + */ + @Nullable + public static String checkDifferences(final IBaseDataObject actualIbdo, final List actualChildren, final String identifier, + final DiffCheckConfiguration options) { + Validate.notNull(actualIbdo, "Required: actualIbdo not null"); + Validate.notNull(actualChildren, "Required: actualChildren not null"); + Validate.notNull(identifier, "Required: identifier not null"); + Validate.notNull(options, "Required: options not null"); + Validate.notNull(options.getLenientExpectationElement(), "Required: LenientExpectationElement not null"); + + final List parentDifferences = new ArrayList<>(); + final List childDifferences = new ArrayList<>(); + IBaseDataObjectDiffHelper.diff(actualIbdo, actualChildren, identifier, parentDifferences, childDifferences, options); + + return buildDifferences(parentDifferences, childDifferences, identifier); + } + + /** + * Build the string of differences/errors + * + * @param parentDifferences differences in the parent ibdo + * @param childDifferences differences in the children ibdos + * @param identifier to highlight any differences in logs + * @return the string of differences, or null if there aren't any + */ + @Nullable + private static String buildDifferences(final List parentDifferences, + final List childDifferences, + final String identifier) { + + Stream parentStream = (parentDifferences == null ? Stream.empty() : parentDifferences.stream()) + .flatMap(diff -> Arrays.stream(diff.split("\\r?\\n"))) + .map(line -> identifier + ": Parent Diff: " + line); + + Stream childStream = (childDifferences == null ? Stream.empty() : childDifferences.stream()) + .flatMap(diff -> Arrays.stream(diff.split("\\r?\\n"))) + .map(line -> identifier + ": Child Diff: " + line); + + String result = Stream.concat(parentStream, childStream) + .collect(Collectors.joining(StringUtils.LF)); + + return result.isEmpty() ? null : result; } } diff --git a/src/test/java/emissary/util/PlaceComparisonHelperTest.java b/src/test/java/emissary/util/PlaceComparisonHelperTest.java index b179693811..9685502794 100644 --- a/src/test/java/emissary/util/PlaceComparisonHelperTest.java +++ b/src/test/java/emissary/util/PlaceComparisonHelperTest.java @@ -141,4 +141,72 @@ void testCheckDifferences() { assertNotNull(PlaceComparisonHelper.checkDifferences( ibdoOldPlace, ibdoForNewPlaceTwoChanges, oldResultsTwoChanges, newResultsTwoChanges, identifier, DIFF_OPTIONS)); } + + + @Test + void testGetPlaceToCompareWithNoConfiguration() throws Exception { + final Configurator configurator1 = ConfigUtil.getConfigInfo(new ByteArrayInputStream(configurationBytes)); + assertNull(PlaceComparisonHelper.getPlaceToCompare(configurator1), + "Place to compare should be null when PLACE_TO_COMPARE is not configured"); + } + + @Test + void testMultipleChildrenComparison() { + final IBaseDataObject parentOld = new BaseDataObject(); + final IBaseDataObject parentNew = new BaseDataObject(); + + final List oldChildren = new ArrayList<>(); + final List newChildren = new ArrayList<>(); + + // Add multiple children with different data + for (int i = 0; i < 3; i++) { + final IBaseDataObject oldChild = new BaseDataObject(); + oldChild.setData(String.format("old-data-%d", i).getBytes()); + oldChildren.add(oldChild); + + final IBaseDataObject newChild = new BaseDataObject(); + newChild.setData(String.format("old-data-%d", i).getBytes()); + newChildren.add(newChild); + } + + // Should be no differences + assertNull(PlaceComparisonHelper.checkDifferences( + parentOld, parentNew, oldChildren, newChildren, "multi-test", DIFF_OPTIONS)); + + // Modify one child + newChildren.get(1).setData("different-data".getBytes()); + assertNotNull(PlaceComparisonHelper.checkDifferences( + parentOld, parentNew, oldChildren, newChildren, "multi-test", DIFF_OPTIONS)); + } + + @Test + void testChildrenCountMismatch() { + final IBaseDataObject parentOld = new BaseDataObject(); + final IBaseDataObject parentNew = new BaseDataObject(); + + final List oldChildren = new ArrayList<>(); + final List newChildren = new ArrayList<>(); + + oldChildren.add(new BaseDataObject()); + oldChildren.add(new BaseDataObject()); + + newChildren.add(new BaseDataObject()); + + // Different count should result in differences + assertNotNull(PlaceComparisonHelper.checkDifferences( + parentOld, parentNew, oldChildren, newChildren, "count-test", DIFF_OPTIONS)); + } + + @Test + void testEmptyChildrenLists() { + final IBaseDataObject parentOld = new BaseDataObject(); + final IBaseDataObject parentNew = new BaseDataObject(); + + final List oldChildren = new ArrayList<>(); + final List newChildren = new ArrayList<>(); + + // Both empty - no differences + assertNull(PlaceComparisonHelper.checkDifferences( + parentOld, parentNew, oldChildren, newChildren, "empty-test", DIFF_OPTIONS)); + } } From 67999e38f93c7a7b3ce9948b54bad93069eb885d Mon Sep 17 00:00:00 2001 From: dev-mlb <19797865+dev-mlb@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:32:18 -0400 Subject: [PATCH 2/2] update verifyOs to use OSReleaseUtil --- .../core/IBaseDataObjectDiffHelper.java | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java b/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java index 3b74551e45..7195032f7b 100644 --- a/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java +++ b/src/test/java/emissary/core/IBaseDataObjectDiffHelper.java @@ -2,6 +2,7 @@ import emissary.core.channels.SeekableByteChannelFactory; import emissary.core.constants.IbdoXmlElementNames; +import emissary.util.os.OSReleaseUtil; import jakarta.annotation.Nullable; import org.apache.commons.collections4.CollectionUtils; @@ -57,10 +58,6 @@ public class IBaseDataObjectDiffHelper { - // Environment/OS isolation verification targets - private static final String SYSTEM_OS_RELEASE = System.getProperty("os.name", "").toLowerCase(Locale.getDefault()); - private static final String MAJOR_OS_VERSION = System.getProperty("os.version", ""); - // Centralized message template definitions private static final String DIFF_NOT_NULL_MSG = "Required: differences not null"; private static final String ID_NOT_NULL_MSG = "Required: identifier not null"; @@ -803,21 +800,36 @@ protected static boolean verifyOs(Element element, List differences) { } String os = specifiedOs.getValue().toLowerCase(Locale.getDefault()); + boolean isMatchingOs; switch (os) { case "ubuntu": + isMatchingOs = OSReleaseUtil.isUbuntu(); + break; case "centos": + isMatchingOs = OSReleaseUtil.isCentOs(); + break; case "rhel": + isMatchingOs = OSReleaseUtil.isRhel(); + break; case "mac": - if (specifiedVersion != null) { - return os.equalsIgnoreCase(SYSTEM_OS_RELEASE) && specifiedVersion.getValue().equals(MAJOR_OS_VERSION); - } else { - return os.equalsIgnoreCase(SYSTEM_OS_RELEASE); - } + isMatchingOs = OSReleaseUtil.isMac(); + break; default: differences.add(String.format("Unsupported or mistyped os-release target '%s' found in element <%s>", specifiedOs.getValue(), element.getName())); return false; } + + if (!isMatchingOs) { + return false; + } + + // Check version match if specified + if (specifiedVersion != null) { + return specifiedVersion.getValue().equals(OSReleaseUtil.getMajorReleaseVersion()); + } + + return true; } protected static String formatErr(Element meta, String key, String reason, String actual, String expected) {