1 package emissary.test.core.junit5;
2
3 import emissary.core.DataObjectFactory;
4 import emissary.core.Family;
5 import emissary.core.IBaseDataObject;
6 import emissary.kff.KffDataObjectHandler;
7 import emissary.place.IServiceProviderPlace;
8 import emissary.test.core.junit5.LogbackTester.SimplifiedLogEvent;
9 import emissary.util.io.ResourceReader;
10 import emissary.util.os.OSReleaseUtil;
11 import emissary.util.xml.JDOMUtil;
12
13 import com.google.errorprone.annotations.ForOverride;
14 import jakarta.annotation.Nullable;
15 import jakarta.xml.bind.DatatypeConverter;
16 import org.apache.commons.collections4.CollectionUtils;
17 import org.apache.commons.io.IOUtils;
18 import org.apache.commons.lang3.StringUtils;
19 import org.jdom2.Attribute;
20 import org.jdom2.DataConversionException;
21 import org.jdom2.Document;
22 import org.jdom2.Element;
23 import org.junit.jupiter.api.AfterEach;
24 import org.junit.jupiter.api.BeforeEach;
25 import org.junit.jupiter.params.ParameterizedTest;
26 import org.junit.jupiter.params.provider.Arguments;
27 import org.junit.jupiter.params.provider.MethodSource;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
30
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.nio.charset.StandardCharsets;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collections;
37 import java.util.List;
38 import java.util.stream.Stream;
39
40 import static org.junit.jupiter.api.Assertions.assertEquals;
41 import static org.junit.jupiter.api.Assertions.assertFalse;
42 import static org.junit.jupiter.api.Assertions.assertNotNull;
43 import static org.junit.jupiter.api.Assertions.assertNull;
44 import static org.junit.jupiter.api.Assertions.assertTrue;
45 import static org.junit.jupiter.api.Assertions.fail;
46
47 public abstract class ExtractionTest extends UnitTest {
48
49 protected static final Logger logger = LoggerFactory.getLogger(ExtractionTest.class);
50
51 private static final List<IBaseDataObject> NO_ATTACHMENTS = Collections.emptyList();
52 private static final byte[] INCORRECT_VIEW_MESSAGE = "This is the incorrect view, the place should not have processed this view".getBytes();
53
54
55
56
57 protected List<SimplifiedLogEvent> actualSimplifiedLogEvents;
58
59 protected KffDataObjectHandler kff =
60 new KffDataObjectHandler(KffDataObjectHandler.TRUNCATE_KNOWN_DATA, KffDataObjectHandler.SET_FORM_WHEN_KNOWN,
61 KffDataObjectHandler.SET_FILE_TYPE);
62 @Nullable
63 protected IServiceProviderPlace place = null;
64 @Nullable
65 private static final String SYSTEM_OS_RELEASE;
66 @Nullable
67 private static final String MAJOR_OS_VERSION;
68
69 static {
70 if (OSReleaseUtil.isUbuntu()) {
71 SYSTEM_OS_RELEASE = "ubuntu";
72 MAJOR_OS_VERSION = OSReleaseUtil.getMajorReleaseVersion();
73 } else if (OSReleaseUtil.isCentOs()) {
74 SYSTEM_OS_RELEASE = "centos";
75 MAJOR_OS_VERSION = OSReleaseUtil.getMajorReleaseVersion();
76 } else if (OSReleaseUtil.isRhel()) {
77 SYSTEM_OS_RELEASE = "rhel";
78 MAJOR_OS_VERSION = OSReleaseUtil.getMajorReleaseVersion();
79 } else if (OSReleaseUtil.isMac()) {
80 SYSTEM_OS_RELEASE = "mac";
81 MAJOR_OS_VERSION = OSReleaseUtil.getMajorReleaseVersion();
82 } else {
83 SYSTEM_OS_RELEASE = null;
84 MAJOR_OS_VERSION = null;
85 }
86 }
87
88 @BeforeEach
89 public void setUpPlace() throws Exception {
90 place = createPlace();
91 }
92
93 @AfterEach
94 public void tearDownPlace() {
95 if (place != null) {
96 place.shutDown();
97 place = null;
98 }
99 }
100
101
102
103
104 public abstract IServiceProviderPlace createPlace() throws IOException;
105
106 public static Stream<? extends Arguments> data() {
107 return getMyTestParameterFiles(ExtractionTest.class);
108 }
109
110
111
112
113
114
115
116
117
118
119 @ForOverride
120 protected String getInitialForm(final String resource) {
121 return resource.replaceAll("^.*/([^/@]+)(@\\d+)?\\.dat$", "$1");
122 }
123
124
125
126
127
128
129 protected String getLogbackLoggerName() {
130 return null;
131 }
132
133 @ParameterizedTest
134 @MethodSource("data")
135 public void testExtractionPlace(String resource) {
136 logger.debug("Running {} test on resource {}", place.getClass().getName(), resource);
137
138
139 Document controlDoc = getAnswerDocumentFor(resource);
140 if (controlDoc == null) {
141 fail("No answers provided for test " + resource);
142 }
143
144 try (InputStream doc = new ResourceReader().getResourceAsStream(resource)) {
145 byte[] data = IOUtils.toByteArray(doc);
146 String initialForm = getInitialForm(resource);
147 IBaseDataObject payload = DataObjectFactory.getInstance(data, resource, initialForm);
148 setupPayload(payload, controlDoc);
149 processPreHook(payload, controlDoc);
150 List<IBaseDataObject> attachments = processHeavyDutyHook(place, payload);
151 processPostHook(payload, attachments);
152 checkAnswersPreHook(controlDoc, payload, attachments, resource);
153 checkAnswers(controlDoc, payload, attachments, resource);
154 checkAnswersPostHook(controlDoc, payload, attachments, resource);
155 } catch (Exception ex) {
156 logger.error("Error running test {}", resource, ex);
157 fail("Cannot run test " + resource, ex);
158 }
159 }
160
161 protected void processPreHook(IBaseDataObject payload, Document controlDoc) {
162
163 }
164
165 protected void processPostHook(IBaseDataObject payload, List<IBaseDataObject> attachments) {
166
167 }
168
169 protected List<IBaseDataObject> processHeavyDutyHook(IServiceProviderPlace place, IBaseDataObject payload) throws Exception {
170 if (getLogbackLoggerName() == null) {
171 actualSimplifiedLogEvents = new ArrayList<>();
172 return place.agentProcessHeavyDuty(payload);
173 } else {
174 try (LogbackTester logbackTester = new LogbackTester(getLogbackLoggerName())) {
175 final List<IBaseDataObject> attachments = place.agentProcessHeavyDuty(payload);
176 actualSimplifiedLogEvents = logbackTester.getSimplifiedLogEvents();
177 return attachments;
178 }
179 }
180 }
181
182 protected void checkAnswersPreHook(Document answers, IBaseDataObject payload, List<IBaseDataObject> attachments, String tname) {
183
184 }
185
186 protected void checkAnswersPreHook(Element answers, IBaseDataObject payload, IBaseDataObject attachment, String tname) {
187
188 }
189
190
191
192
193
194
195
196
197 protected void checkAnswersPreHookLogEvents(List<SimplifiedLogEvent> simplifiedLogEvents) {
198
199 }
200
201 protected void checkAnswersPostHook(Document answers, IBaseDataObject payload, List<IBaseDataObject> attachments, String tname) {
202
203 }
204
205 protected void checkAnswersPostHook(Element answers, IBaseDataObject payload, IBaseDataObject attachment, String tname) {
206
207 }
208
209 protected void checkAnswers(Document answers, IBaseDataObject payload, List<IBaseDataObject> attachments, String tname)
210 throws DataConversionException {
211 Element root = answers.getRootElement();
212 Element parent = root.getChild("answers");
213 if (parent == null) {
214 parent = root;
215 }
216
217
218 checkAnswers(parent, payload, attachments, tname);
219
220
221 for (int attNum = 1; attNum <= attachments.size(); attNum++) {
222 String atname = tname + Family.SEP + attNum;
223 Element el = parent.getChild("att" + attNum);
224 if (el != null) {
225 checkAnswersPreHook(el, payload, attachments.get(attNum - 1), atname);
226 checkAnswers(el, attachments.get(attNum - 1), null, atname);
227 checkAnswersPostHook(el, payload, attachments.get(attNum - 1), atname);
228 }
229 }
230 }
231
232 protected void checkAnswers(Element el, IBaseDataObject payload, @Nullable List<IBaseDataObject> attachments, String tname)
233 throws DataConversionException {
234
235 int numAtt = JDOMUtil.getChildIntValue(el, "numAttachments");
236 long numAttElements = el.getChildren().stream().filter(c -> c.getName().startsWith("att")).count();
237
238 if (numAtt > -1) {
239 assertEquals(numAtt, attachments != null ? attachments.size() : 0,
240 String.format("Expected <numAttachments> in %s not equal to number of att in payload.", tname));
241 } else if (numAtt == -1 && numAttElements > 0) {
242 assertEquals(numAttElements, attachments != null ? attachments.size() : 0,
243 String.format("Expected <att#> in %s not equal to number of att in payload.", tname));
244 } else {
245 if (attachments != null && !attachments.isEmpty()) {
246 fail(String.format("%d attachments in payload with no count in answer xml, add matching <numAttachments> count for %s",
247 attachments.size(), tname));
248 }
249 }
250
251 for (Element currentForm : el.getChildren("currentForm")) {
252 String cf = currentForm.getTextTrim();
253 if (cf != null) {
254 Attribute index = currentForm.getAttribute("index");
255 if (index != null) {
256 assertEquals(payload.currentFormAt(index.getIntValue()), cf,
257 String.format("Current form '%s' not found at position [%d] in %s, %s", cf, index.getIntValue(), tname,
258 payload.getAllCurrentForms()));
259 } else {
260 assertTrue(payload.searchCurrentForm(cf) > -1,
261 String.format("Current form %s not found in %s, %s", cf, tname, payload.getAllCurrentForms()));
262 }
263 }
264 }
265
266 String cf = el.getChildTextTrim("currentForm");
267 if (cf != null) {
268 assertTrue(payload.searchCurrentForm(cf) > -1,
269 String.format("Current form '%s' not found in %s, %s", cf, tname, payload.getAllCurrentForms()));
270 }
271
272 String ft = el.getChildTextTrim("fileType");
273 if (ft != null) {
274 assertEquals(ft, payload.getFileType(), String.format("Expected File Type '%s' in %s", ft, tname));
275 }
276
277 int cfsize = JDOMUtil.getChildIntValue(el, "currentFormSize");
278 if (cfsize > -1) {
279 assertEquals(cfsize, payload.currentFormSize(), "Current form size in " + tname);
280 }
281
282 String classification = el.getChildTextTrim("classification");
283 if (classification != null) {
284 assertEquals(classification, payload.getClassification(),
285 String.format("Classification in '%s' is '%s', not expected '%s'", tname, payload.getClassification(), classification));
286 }
287
288 for (Element dataLength : el.getChildren("dataLength")) {
289 if (verifyOs(dataLength)) {
290 int length;
291 try {
292 length = Integer.parseInt(dataLength.getValue());
293 } catch (NumberFormatException e) {
294 length = -1;
295 }
296 if (length > -1) {
297 assertEquals(length, payload.dataLength(), "Data length in " + tname);
298 }
299 }
300 }
301
302 String shortName = el.getChildTextTrim("shortName");
303 if (shortName != null && shortName.length() > 0) {
304 assertEquals(shortName, payload.shortName(), "Shortname does not match expected in " + tname);
305 }
306
307 String fontEncoding = el.getChildTextTrim("fontEncoding");
308 if (StringUtils.isNotBlank(fontEncoding)) {
309 assertEquals(fontEncoding, payload.getFontEncoding(), "Font encoding does not match expected in " + tname);
310 }
311
312 String broke = el.getChildTextTrim("broken");
313 if (broke != null && broke.length() > 0) {
314 assertEquals(broke, payload.isBroken() ? "true" : "false", "Broken status in " + tname);
315 }
316
317 String procError = el.getChildTextTrim("procError");
318 if (procError != null && !procError.isEmpty()) {
319 assertNotNull(payload.getProcessingError(),
320 String.format("Expected processing error '%s' in %s", procError, tname));
321
322
323 String shortProcErrMessage = payload.getProcessingError().replaceAll("\n", ";");
324 assertEquals(procError, shortProcErrMessage, "Processing Error does not match expected in " + tname);
325 }
326
327
328 for (Element meta : el.getChildren("meta")) {
329 if (verifyOs(meta)) {
330 String key = meta.getChildTextTrim("name");
331 checkForMissingNameElement("meta", key, tname);
332 checkStringValue(meta, payload.getStringParameter(key), tname);
333 }
334 }
335
336
337 for (Element meta : el.getChildren("nometa")) {
338 if (verifyOs(meta)) {
339 String key = meta.getChildTextTrim("name");
340 checkForMissingNameElement("nometa", key, tname);
341 assertFalse(payload.hasParameter(key),
342 String.format("Metadata element '%s' in '%s' should not exist, but has value of '%s'", key, tname,
343 payload.getStringParameter(key)));
344 }
345 }
346
347
348
349
350 for (Element dataEl : el.getChildren("data")) {
351 if (verifyOs(dataEl)) {
352 byte[] payloadData = payload.data();
353 checkStringValue(dataEl, new String(payloadData), tname);
354 }
355 }
356
357
358 for (Element view : el.getChildren("view")) {
359 if (verifyOs(view)) {
360 String viewName = view.getChildTextTrim("name");
361 String lengthStr = view.getChildTextTrim("length");
362 byte[] viewData = payload.getAlternateView(viewName);
363 assertNotNull(viewData, String.format("Alternate View '%s' is missing in %s", viewName, tname));
364 if (lengthStr != null) {
365 assertEquals(Integer.parseInt(lengthStr), viewData.length,
366 String.format("Length of Alternate View '%s' is wrong in %s", viewName, tname));
367 }
368 checkStringValue(view, new String(viewData), tname);
369 }
370 }
371
372
373 for (Element view : el.getChildren("noview")) {
374 if (verifyOs(view)) {
375 String viewName = view.getChildTextTrim("name");
376 byte[] viewData = payload.getAlternateView(viewName);
377 assertNull(viewData, String.format("Alternate View '%s' is present, but should not be, in %s", viewName, tname));
378 }
379 }
380
381
382 int extractCount = JDOMUtil.getChildIntValue(el, "extractCount");
383 long numExtractElements =
384 el.getChildren().stream().filter(c -> c.getName().startsWith("extract") && !c.getName().startsWith("extractCount")).count();
385 if (payload.hasExtractedRecords()) {
386 List<IBaseDataObject> extractedChildren = payload.getExtractedRecords();
387 int foundCount = extractedChildren.size();
388
389 if (extractCount > -1) {
390 assertEquals(extractCount, foundCount,
391 String.format("Expected <extractCount> in %s not equal to number of extracts in payload.", tname));
392 } else if (extractCount == -1 && numExtractElements > 0) {
393 assertEquals(numExtractElements, foundCount,
394 String.format("Expected <extract#> in %s not equal to number of extracts in payload.", tname));
395 } else {
396 fail(String.format("%d extracts in payload with no count in answer xml, add matching <extractCount> count for %s",
397 foundCount, tname));
398 }
399
400 int attNum = 1;
401 for (IBaseDataObject extractedChild : extractedChildren) {
402 Element extel = el.getChild("extract" + attNum);
403 if (extel != null) {
404 checkAnswers(extel, extractedChild, NO_ATTACHMENTS, String.format("%s::extract%d", tname, attNum));
405 }
406 attNum++;
407 }
408 } else {
409 if (extractCount > -1) {
410 assertEquals(0, extractCount,
411 String.format("No extracted children in '%s' when <extractCount> is %d", tname, extractCount));
412 } else if (numExtractElements > 0) {
413 assertEquals(0, numExtractElements,
414 String.format("No extracted children in '%s' when <extract#> is %d", tname, numExtractElements));
415 }
416 }
417 }
418
419 private static void checkForMissingNameElement(String parentTag, String key, String tname) {
420 if (key == null) {
421 fail(String.format("The element %s has a problem in %s: does not have a child name element", parentTag, tname));
422 }
423 }
424
425 protected void checkStringValue(Element meta, String data, String tname) {
426 String key = meta.getChildTextTrim("name");
427 String value = meta.getChildText("value");
428 String matchMode = "equals";
429 Attribute mm = meta.getAttribute("matchMode");
430
431 if (value == null) {
432 return;
433 }
434
435 if (mm != null) {
436 matchMode = mm.getValue();
437 }
438
439 if (matchMode.equals("equals")) {
440 assertEquals(value, data,
441 String.format("%s element '%s' problem in %s value '%s' does not equal '%s'", meta.getName(), key, tname, data, value));
442 } else if (matchMode.equals("index") || matchMode.equals("contains")) {
443 assertTrue(data.contains(value),
444 String.format("%s element '%s' problem in %s value '%s' does not index '%s'", meta.getName(), key, tname, data, value));
445 } else if (matchMode.equals("!index") || matchMode.equals("!contains")) {
446 assertFalse(data.contains(value),
447 String.format("%s element '%s' problem in %s value '%s' should not be indexed in '%s'", meta.getName(), key, tname, value, data));
448 } else if (matchMode.equals("match")) {
449 assertTrue(data.matches(value),
450 String.format("%s element '%s' problem in %s value '%s' does not match '%s'", meta.getName(), key, tname, data, value));
451 } else if (matchMode.equals("base64")) {
452
453
454 value = new String(DatatypeConverter.parseBase64Binary(value));
455 assertEquals(value, data,
456 String.format("%s element '%s' problem in %s value '%s' does not match '%s'", meta.getName(), key, tname, data, value));
457 } else if ("collection".equalsIgnoreCase(matchMode)) {
458 Attribute separatorAttribute = meta.getAttribute("collectionSeparator");
459 String separator = null != separatorAttribute ? separatorAttribute.getValue() : ",";
460
461 List<String> expectedValues = Arrays.asList(value.split(separator));
462 List<String> actualValues = Arrays.asList(data.split(separator));
463 assertTrue(CollectionUtils.isEqualCollection(expectedValues, actualValues),
464 String.format(
465 "%s element '%s' problem in %s did not have equal collection, value '%s' does not equal '%s' split by separator '%s'",
466 meta.getName(), key, tname, data, value, separator));
467
468 } else {
469 fail(String.format("Problematic matchMode specified for test '%s' on %s in element %s", matchMode, key, meta.getName()));
470 }
471 }
472
473 protected boolean verifyOs(Element element) {
474 Attribute specifiedOs = element.getAttribute("os-release");
475 Attribute specifiedVersion = element.getAttribute("os-version");
476 if (specifiedOs != null) {
477 String os = specifiedOs.getValue();
478 switch (os) {
479 case "ubuntu":
480 case "centos":
481 case "rhel":
482 case "mac":
483 if (specifiedVersion != null) {
484 return os.equals(SYSTEM_OS_RELEASE) && specifiedVersion.getValue().equals(MAJOR_OS_VERSION);
485 } else {
486
487 return os.equals(SYSTEM_OS_RELEASE);
488 }
489 default:
490 fail("specified OS needs to match ubuntu, centos, rhel, or mac. Provided OS=" + os);
491 }
492 }
493
494 return true;
495 }
496
497 protected void setupPayload(IBaseDataObject payload, Document doc) {
498 kff.hash(payload);
499 Element root = doc.getRootElement();
500 Element setup = root.getChild("setup");
501 boolean didSetFiletype = false;
502 if (setup != null) {
503 List<Element> cfChildren = setup.getChildren("initialForm");
504 if (!cfChildren.isEmpty()) {
505 payload.popCurrentForm();
506 }
507 for (Element cf : cfChildren) {
508 payload.enqueueCurrentForm(cf.getTextTrim());
509 }
510
511 final String classification = setup.getChildTextTrim("classification");
512 if (StringUtils.isNotBlank(classification)) {
513 payload.setClassification(classification);
514 }
515
516 final String fontEncoding = setup.getChildTextTrim("fontEncoding");
517 if (StringUtils.isNotBlank(fontEncoding)) {
518 payload.setFontEncoding(fontEncoding);
519 }
520
521 for (Element meta : setup.getChildren("meta")) {
522 String key = meta.getChildTextTrim("name");
523 String value = meta.getChildTextTrim("value");
524 payload.appendParameter(key, value);
525 }
526
527 for (Element altView : setup.getChildren("altView")) {
528 String name = altView.getChildTextTrim("name");
529 byte[] value = altView.getChildText("value").getBytes(StandardCharsets.UTF_8);
530 payload.addAlternateView(name, value);
531 }
532
533 final String fileType = setup.getChildTextTrim("fileType");
534 if (StringUtils.isNotBlank(fileType)) {
535 payload.setFileType(fileType);
536 didSetFiletype = true;
537 }
538
539 final String inputAlternateView = setup.getChildTextTrim("inputAlternateView");
540 if (StringUtils.isNotBlank(inputAlternateView)) {
541 final byte[] data = payload.data();
542 payload.addAlternateView(inputAlternateView, data);
543 payload.setData(INCORRECT_VIEW_MESSAGE);
544 }
545
546 final String badAlternateView = setup.getChildTextTrim("badAlternateView");
547 if (StringUtils.isNotBlank(badAlternateView)) {
548 payload.addAlternateView(badAlternateView, INCORRECT_VIEW_MESSAGE);
549 }
550 }
551 if (!didSetFiletype) {
552 payload.setFileType(payload.currentForm());
553 }
554 }
555 }