diff --git a/.gitattributes b/.gitattributes index 067124344f..c51d0362b1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,10 @@ configure-from-env text eol=lf # Binary files are left untouched *.jar binary +*.dcm binary + +# NCPDP fixtures delimit with control characters (0x1C/0x1D/0x1E), not line breaks +/server/tests/test-ncpdp-*-input.txt binary + +# EDISerializer only infers delimiters from a bare \n after the segment terminator +/server/tests/test-edi-input.txt text eol=lf diff --git a/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java b/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java index 405550a60f..214e74e7c1 100644 --- a/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java +++ b/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java @@ -49,6 +49,11 @@ public static String prettyPrintXml(String input) throws Exception { return writer.toString(); } + /** Line endings carry no meaning in pretty-printed XML, and the fixtures on disk are CRLF. */ + public static String normalizeLineEndings(String input) { + return input.replaceAll("\r\n|\r", "\n"); + } + public static String convertCRToCRLF(String input) { return input.replaceAll("\r", "\r\n"); } diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTest.java new file mode 100644 index 0000000000..5309cdc172 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTest.java @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2017 Mirth Corporation +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package com.mirth.connect.plugins.datatypes.dicom; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Serializes every sample in tests/dicom and compares it against the committed XML, which is the + * serializer's own output and so is already indented. Drop a new .dcm and its .xml into that + * directory and it is picked up without touching this class; see the README there for provenance. + */ +@RunWith(Parameterized.class) +public class DICOMSerializerTest { + + // server/build.gradle runs tests with workingDir = projectDir + private static final File SAMPLES = new File("tests/dicom"); + + @Parameters(name = "{0}") + public static List samples() { + String[] names = SAMPLES.list((dir, name) -> name.endsWith(".dcm")); + assertTrue("No DICOM samples found in " + SAMPLES.getAbsolutePath(), names != null && names.length > 0); + Arrays.sort(names); + return Arrays.asList(names); + } + + @Parameter + public String name; + + @Test + public void toXml() throws Exception { + // the committed XML is of the header alone; pixel data would dwarf it + byte[] header = DICOMSerializer.removePixelData(FileUtils.readFileToByteArray(new File(SAMPLES, name))); + String actual = new DICOMSerializer().toXML(Base64.getEncoder().encodeToString(header)); + String expected = FileUtils.readFileToString(new File(SAMPLES, StringUtils.removeEnd(name, ".dcm") + ".xml"), UTF_8); + + assertEquals(expected, actual); + } +} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTests.java deleted file mode 100644 index db549d4948..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTests.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.dicom; - -import java.io.File; - -import junit.framework.Assert; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.net.util.Base64; -import org.junit.Test; - -import com.mirth.connect.model.converters.TestUtil; - -public class DICOMSerializerTests { - @Test - public void testToXml1() throws Exception { - String input = Base64.encodeBase64String(FileUtils.readFileToByteArray(new File("tests/test-dicom-input-1.dcm"))); - String output = FileUtils.readFileToString(new File("tests/test-dicom-output-1.xml")); - DICOMSerializer serializer = new DICOMSerializer(); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void testToXml2() throws Exception { - String input = Base64.encodeBase64String(FileUtils.readFileToByteArray(new File("tests/test-dicom-input-2.dcm"))); - String output = FileUtils.readFileToString(new File("tests/test-dicom-output-2.xml")); - DICOMSerializer serializer = new DICOMSerializer(); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void testToXml3() throws Exception { - String input = Base64.encodeBase64String(FileUtils.readFileToByteArray(new File("tests/test-dicom-input-3.dcm"))); - String output = FileUtils.readFileToString(new File("tests/test-dicom-output-3.xml")); - DICOMSerializer serializer = new DICOMSerializer(); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } -} \ No newline at end of file diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMTests.java deleted file mode 100644 index c921f3b496..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMTests.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.dicom; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -import org.xml.sax.SAXException; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.Stopwatch; - -/** - * Created by IntelliJ IDEA. User: dans Date: Aug 6, 2007 Time: 2:00:06 PM To change this template - * use File | Settings | File Templates. - */ -public class DICOMTests { - public static void main(String[] args) { - String testMessage = ""; - ArrayList testFiles = new ArrayList(); - testFiles.add("C:\\abdominal.dcm"); - //testFiles.add("C:\\abdominal.dcm"); - //testFiles.add("C:\\brain.dcm"); - //String[] a = new String[1]; - //a[0] = "c:\\ankle.dcm"; - - //ImageJ ij = new ImageJ(null,ImageJ.EMBEDDED); - //ImageJ.main(a); - -// Client client = new Client("https://localhost:8443"); -// Attachment a = new Attachment(); -// a.setAttachmentId("TEST1"); -// a.setMessageId("TEST2"); -// a.setSize(100); -// a.setType("TESTING"); -// try { -// client.login("admin", "admin", "1.5.0"); -// a.setData(getBytesFromFile(new File("C:\\abdominal.dcm"))); -//// client.insertAttachment(a); -// -// Attachment a1 = client.getAttachment("TEST1"); -// List a3 = client.getAttachmentsByMessageId("TEST2"); -// Attachment a2 = a3.get(0); -// if(a1.equals(a2)){ -// System.out.println("They are the same"); -// } -// System.out.println("First:" + a1.toString()); -// -// System.out.println("Second:" + a2.toString()); -// -// -// } -// catch(Exception e) { -// e.printStackTrace(); -// } - -// Iterator iterator = testFiles.iterator(); -// while(iterator.hasNext()){ -// String fileName = (String) iterator.next(); -// try { -// testMessage = new String(getBytesFromFile(new File(fileName))); -// System.out.println("Processing test file:" + fileName); -// //System.out.println(testMessage); -// } catch (IOException e) { -// e.printStackTrace(); -// } -// try { -// -// long totalExecutionTime = 0; -// int iterations = 1; -// for (int i = 0; i < iterations; i++) { -// totalExecutionTime+=runTest(testMessage); -// } -// -// //System.out.println("Execution time average: " + totalExecutionTime/iterations + " ms"); -// } -// // System.out.println(new X12Serializer().toXML("SEG*1*2**4*5")); -// catch (SAXException e) { -// e.printStackTrace(); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } - } - - private static long runTest(String testMessage) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); -// Properties properties = new Properties(); -// properties.put("includePixelData","no"); -// properties.put("isEncoded","no"); - stopwatch.start(); - DICOMSerializer serializer = new DICOMSerializer(null); -// String xmloutput = serializer.toXML(testMessage); - //Dcm2Xml dcm2xml = new Dcm2Xml(); - File xmlOut = File.createTempFile("test", "xml"); - File dcmInput = new File("c:\\US-PAL-8-10x-echo.dcm"); - try { - // dcm2xml.convert(dcmInput,xmlOut); - } catch (Exception e) { - e.printStackTrace(); - } - File dcmOutput = File.createTempFile("test", "dcm"); - String[] args = new String[4]; - args[0] = "-x"; - args[1] = xmlOut.getAbsolutePath(); - args[2] = "-o"; - args[3] = "c:\\dcmOutput.dcm"; - //Xml2Dcm.main(args); - // TO XML again - File input2 = new File("c:\\dcmOutput.dcm"); - try { - /// dcm2xml.convert(input2,xmlOut); - } catch (Exception e) { - e.printStackTrace(); - } - dcmOutput = File.createTempFile("test", "dcm"); - args = new String[4]; - args[0] = "-x"; - args[1] = xmlOut.getAbsolutePath(); - args[2] = "-o"; - args[3] = "c:\\dcmOutput2.dcm"; - // Xml2Dcm.main(args); - //System.out.println(xmloutput); -// DocumentSerializer docser = new DocumentSerializer(); -// docser.setPreserveSpace(true); -// -// Document doc = docser.fromXML(xmloutput); -// XMLReader xr = XMLReaderFactory.createXMLReader(); - String results = ""; //= serializer.fromXML(xmloutput); -// String xmloutput2 = serializer.toXML(results); -// String results2 = serializer.fromXML(xmloutput2); - System.out.println("testing..."); - if (results.replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = results.replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("Test Failed!"); - } - return stopwatch.toValue(); - } - - // Returns the contents of the file in a byte array. - private static byte[] getBytesFromFile(File file) throws IOException { - InputStream is = new FileInputStream(file); - - // Get the size of the file - long length = file.length(); - - // You cannot create an array using a long type. - // It needs to be an int type. - // Before converting to an int type, check - // to ensure that file is not larger than Integer.MAX_VALUE. - if (length > Integer.MAX_VALUE) { - // File is too large - } - - // Create the byte array to hold the data - byte[] bytes = new byte[(int) length]; - - // Read in the bytes - int offset = 0; - int numRead = 0; - while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - - // Ensure all the bytes have been read in - if (offset < bytes.length) { - throw new IOException("Could not completely read file " + file.getName()); - } - - // Close the input stream and return bytes - is.close(); - return bytes; - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java index b136af186d..76be7b099f 100644 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java @@ -4,11 +4,15 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.File; + +import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Test; import org.xml.sax.SAXParseException; import com.mirth.connect.donkey.model.message.MessageSerializerException; +import com.mirth.connect.model.converters.TestUtil; import com.mirth.connect.model.datatype.SerializerProperties; public class EDISerializerTest { @@ -86,6 +90,31 @@ public void testFromXMLPadsEveryIsa() throws Exception { private static String validX12 = "ISA*00* *00* *ZZ*SUBMITTERID *ZZ*PAYERID *260330*1430*^*00501*000000001*0*T*:~" + "GS*HC*SUBMITTERID*PAYERID*20260330*1430*1*X*005010X222A1~"; + @Test + public void testToXml() throws Exception { + assertXmlEquals(read("test-edi-output.xml"), serializer.toXML(read("test-edi-input.txt"))); + } + + @Test + public void testFromXml() throws Exception { + assertEquals(read("test-edi-input.txt"), serializer.fromXML(read("test-edi-output.xml"))); + } + + /** Issue 1597: the serializer fills in elements the XML leaves out. */ + @Test + public void testIssue1597fromXML() throws Exception { + assertEquals(read("test-edi-input.txt"), serializer.fromXML(read("test-1597-input-missing-elements.xml"))); + } + + private static String read(String name) throws Exception { + return FileUtils.readFileToString(new File("tests/" + name), "UTF-8"); + } + + /** toXML returns one unindented line with an XML declaration; the fixtures are readable. */ + private static void assertXmlEquals(String expected, String actual) throws Exception { + assertEquals(TestUtil.normalizeLineEndings(expected), TestUtil.normalizeLineEndings(TestUtil.prettyPrintXml(actual))); + } + private static String validXml = "\r\n" + "bar"; diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTests.java deleted file mode 100644 index 3969ceaad8..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.edi; - -import java.io.File; - -import junit.framework.Assert; - -import org.apache.commons.io.FileUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class EDISerializerTests { - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-edi-input.txt"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-edi-output.xml"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.toXML(input)); - } - - @Test - public void testFromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-edi-output.xml"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-edi-input.txt"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - /* - * Checks if serializer adds missing elements when going from XML to EDI/X12. - */ - @Test - public void testIssue1597fromXML() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-1597-input-missing-elements.xml"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-1597-output.txt"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void testIssue1597toXML() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-1597-output.txt"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-1597-input.xml"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.toXML(input)); - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests.java deleted file mode 100644 index 592b80bcdd..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.edi; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import junit.framework.Assert; - -import org.xml.sax.SAXException; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.DocumentSerializer; -import com.mirth.connect.model.converters.Stopwatch; - -public class X12Tests { - public static void main(String[] args) { - String testMessage = ""; - try { - testMessage = new String(getBytesFromFile(new File(args[0]))); - System.out.println(testMessage); - } catch (IOException e) { - e.printStackTrace(); - } - try { - - long totalExecutionTime = 0; - int iterations = 100; - for (int i = 0; i < iterations; i++) { - totalExecutionTime += runTest(testMessage); - } - - System.out.println("Execution time average: " + totalExecutionTime / iterations + " ms"); - } - // System.out.println(new X12Serializer().serialize("SEG*1*2**4*5")); - catch (SAXException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - private static long runTest(String testMessage) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); - stopwatch.start(); - EDISerializer serializer = new EDISerializer((new EDIDataTypeProperties()).getSerializerProperties()); - String xmloutput = serializer.toXML(testMessage); - DocumentSerializer docser = new DocumentSerializer(); - String x12 = serializer.fromXML(xmloutput); - stopwatch.stop(); - - // System.out.println(docser.serialize(doc)); // handler.getOutput()); - // System.out.println(x12); - Assert.assertTrue(x12.replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())); - - if (x12.replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = x12.replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("Test Failed!"); - } - return stopwatch.toValue(); - } - - // Returns the contents of the file in a byte array. - private static byte[] getBytesFromFile(File file) throws IOException { - InputStream is = new FileInputStream(file); - try { - // Get the size of the file - long length = file.length(); - - // You cannot create an array using a long type. - // It needs to be an int type. - // Before converting to an int type, check - // to ensure that file is not larger than Integer.MAX_VALUE. - if (length > Integer.MAX_VALUE) { - // File is too large - } - - // Create the byte array to hold the data - byte[] bytes = new byte[(int) length]; - - // Read in the bytes - int offset = 0; - int numRead = 0; - while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - - // Ensure all the bytes have been read in - if (offset < bytes.length) { - throw new IOException("Could not completely read file " + file.getName()); - } - return bytes; - } finally { - // Close the input stream and return bytes - is.close(); - - } - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests2.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests2.java deleted file mode 100644 index 07a5dc0ec8..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests2.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.edi; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.Unmarshaller; - -public class X12Tests2 { - - /** - * @param args - */ - public static void main(String[] args) { - try { - JAXBContext jc = JAXBContext.newInstance("com.mirth.connect.model.edi"); - Unmarshaller unmarshaller = jc.createUnmarshaller(); - // TransactionType collection= - // (TransactionType)((JAXBElement)unmarshaller.unmarshal(X12Test.class.getResourceAsStream("xml/837.4010.X097.xml"))).getValue(); - TransactionType collection = (TransactionType) ((JAXBElement) unmarshaller.unmarshal(X12Tests2.class.getResourceAsStream("xml/997.4010.xml"))).getValue(); - System.out.println(collection.getId() + ": " + collection.getName()); - - HashMap mappings = new LinkedHashMap(); - LoopType loop = collection.getLoop(); - processLoop(loop, mappings); - for (Iterator iter = mappings.entrySet().iterator(); iter.hasNext();) { - Entry element = (Entry) iter.next(); - System.out.println(element.getKey() + ": " + element.getValue()); - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - - private static void addEntry(String name, String description, Map mappings) { - // hack way to format for our ui - name = name.replaceAll("_LOOP", "").replaceAll("-", ""); - if (name.length() > 5) { - name = name.substring(0, name.length() - 4) + "." + trimLeadingZero(name.substring(name.length() - 4, name.length() - 2)) + "." + trimLeadingZero(name.substring(name.length() - 2)); - } else if (name.length() > 3) { - name = name.substring(0, name.length() - 2) + "." + trimLeadingZero(name.substring(name.length() - 2)); - } - mappings.put(name.replace('-', '.'), description); - } - - private static String trimLeadingZero(String name) { - if (name.startsWith("0")) { - return name.substring(1); - } else { - return name; - } - } - - private static void processLoop(LoopType loop, Map mappings) { - addEntry(loop.getXid(), loop.getName(), mappings); - for (Iterator iter = loop.getSegmentOrLoopOrRepeat().iterator(); iter.hasNext();) { - Object element = ((JAXBElement) iter.next()).getValue(); - if (element instanceof SegmentType) { - SegmentType segment = (SegmentType) element; - processSegment(segment, mappings); - } else if (element instanceof LoopType) { - processLoop((LoopType) element, mappings); - } - } - } - - private static void processSegment(SegmentType segment, Map mappings) { - addEntry(segment.getXid(), segment.getName(), mappings); - for (Iterator iter = segment.getElementOrComposite().iterator(); iter.hasNext();) { - Object component = iter.next(); - if (component instanceof CompositeType) { - processComposite((CompositeType) component, mappings); - } else if (component instanceof ElementType) { - ElementType element = (ElementType) component; - addEntry(element.getXid(), element.getName(), mappings); - } - } - } - - private static void processComposite(CompositeType composite, Map mappings) { - addEntry(composite.getDataEle(), composite.getName(), mappings); - for (Iterator iter = composite.getElement().iterator(); iter.hasNext();) { - ElementType element = iter.next(); - addEntry(element.getXid(), element.getName(), mappings); - - } - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/ER7ReaderTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/ER7ReaderTest.java new file mode 100644 index 0000000000..3943a4bda3 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/ER7ReaderTest.java @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Nico Piel + +package com.mirth.connect.plugins.datatypes.hl7v2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import com.mirth.connect.model.converters.XMLPrettyPrinter; + +public class ER7ReaderTest { + + private static Document parse(String message, boolean handleRepetitions, boolean handleSubcomponents) throws Exception { + ER7Reader reader = new ER7Reader(handleRepetitions, handleSubcomponents, "\r"); + StringWriter writer = new StringWriter(); + XMLPrettyPrinter printer = new XMLPrettyPrinter(writer); + printer.setEncodeEntities(true); + reader.setContentHandler(printer); + reader.parse(new InputSource(new StringReader(message))); + return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(writer.toString()))); + } + + private static Document parse(String message) throws Exception { + return parse(message, false, false); + } + + private static String text(Document doc, String xpath) throws Exception { + return XPathFactory.newInstance().newXPath().evaluate(xpath, doc); + } + + private static int count(Document doc, String xpath) throws Exception { + return ((NodeList) XPathFactory.newInstance().newXPath().evaluate(xpath, doc, XPathConstants.NODESET)).getLength(); + } + + @Test + public void testEmptyFields() throws Exception { + Document doc = parse("MSH|^~\\&\rPID|||X|"); + + assertEquals(1, count(doc, "/HL7Message/PID/PID.1")); + assertEquals(0, count(doc, "/HL7Message/PID/PID.1/*")); + assertEquals(1, count(doc, "/HL7Message/PID/PID.2")); + assertEquals("X", text(doc, "/HL7Message/PID/PID.3/PID.3.1")); + // A trailing field separator still produces the final, empty field + assertEquals(1, count(doc, "/HL7Message/PID/PID.4")); + assertEquals(0, count(doc, "/HL7Message/PID/PID.4/*")); + } + + @Test + public void testEmptyComponents() throws Exception { + Document doc = parse("MSH|^~\\&\rPID|^A^"); + + assertEquals("", text(doc, "/HL7Message/PID/PID.1/PID.1.1")); + assertEquals("A", text(doc, "/HL7Message/PID/PID.1/PID.1.2")); + assertEquals(1, count(doc, "/HL7Message/PID/PID.1/PID.1.3")); + assertEquals("", text(doc, "/HL7Message/PID/PID.1/PID.1.3")); + } + + @Test + public void testRepetitionsIgnoredByDefault() throws Exception { + Document doc = parse("MSH|^~\\&\rPID|A~B"); + + assertEquals(1, count(doc, "/HL7Message/PID/PID.1")); + assertEquals("A~B", text(doc, "/HL7Message/PID/PID.1/PID.1.1")); + } + + @Test + public void testHandleRepetitions() throws Exception { + Document doc = parse("MSH|^~\\&\rPID|A~~B^C~", true, false); + + assertEquals(4, count(doc, "/HL7Message/PID/PID.1")); + assertEquals("A", text(doc, "/HL7Message/PID/PID.1[1]/PID.1.1")); + assertEquals(0, count(doc, "/HL7Message/PID/PID.1[2]/*")); + assertEquals("B", text(doc, "/HL7Message/PID/PID.1[3]/PID.1.1")); + assertEquals("C", text(doc, "/HL7Message/PID/PID.1[3]/PID.1.2")); + assertEquals(0, count(doc, "/HL7Message/PID/PID.1[4]/*")); + } + + @Test + public void testSubcomponentsIgnoredByDefault() throws Exception { + Document doc = parse("MSH|^~\\&\rPID|A&B^C"); + + assertEquals("A&B", text(doc, "/HL7Message/PID/PID.1/PID.1.1")); + assertEquals("C", text(doc, "/HL7Message/PID/PID.1/PID.1.2")); + } + + @Test + public void testHandleSubcomponents() throws Exception { + Document doc = parse("MSH|^~\\&\rPID|A&B^C|X&&Y&", false, true); + + assertEquals("A", text(doc, "/HL7Message/PID/PID.1/PID.1.1/PID.1.1.1")); + assertEquals("B", text(doc, "/HL7Message/PID/PID.1/PID.1.1/PID.1.1.2")); + assertEquals("C", text(doc, "/HL7Message/PID/PID.1/PID.1.2")); + + // A field with subcomponents but no component separator is still split + assertEquals("X", text(doc, "/HL7Message/PID/PID.2/PID.2.1/PID.2.1.1")); + assertEquals(1, count(doc, "/HL7Message/PID/PID.2/PID.2.1/PID.2.1.2")); + assertEquals("", text(doc, "/HL7Message/PID/PID.2/PID.2.1/PID.2.1.2")); + assertEquals("Y", text(doc, "/HL7Message/PID/PID.2/PID.2.1/PID.2.1.3")); + assertEquals(1, count(doc, "/HL7Message/PID/PID.2/PID.2.1/PID.2.1.4")); + } + + @Test + public void testCustomEncodingCharacters() throws Exception { + Document doc = parse("MSH#*@!$#APP\rPID#A*B@C$D", true, true); + + assertEquals("#", text(doc, "/HL7Message/MSH/MSH.1")); + assertEquals("*@!$", text(doc, "/HL7Message/MSH/MSH.2")); + assertEquals("APP", text(doc, "/HL7Message/MSH/MSH.3/MSH.3.1")); + assertEquals(2, count(doc, "/HL7Message/PID/PID.1")); + assertEquals("A", text(doc, "/HL7Message/PID/PID.1[1]/PID.1.1")); + assertEquals("B", text(doc, "/HL7Message/PID/PID.1[1]/PID.1.2")); + assertEquals("C", text(doc, "/HL7Message/PID/PID.1[2]/PID.1.1/PID.1.1.1")); + assertEquals("D", text(doc, "/HL7Message/PID/PID.1[2]/PID.1.1/PID.1.1.2")); + } + + @Test + public void testMissingEscapeCharacterIsRestored() throws Exception { + // MIRTH-1544: ^~& is treated as ^~\& + Document doc = parse("MSH|^~&|APP\rPID|A&B", false, true); + + assertEquals("^~\\&", text(doc, "/HL7Message/MSH/MSH.2")); + assertEquals("A", text(doc, "/HL7Message/PID/PID.1/PID.1.1/PID.1.1.1")); + assertEquals("B", text(doc, "/HL7Message/PID/PID.1/PID.1.1/PID.1.1.2")); + } + + @Test + public void testHeaderWithOnlyEncodingCharacters() throws Exception { + assertEquals("^~", text(parse("MSH|^~"), "/HL7Message/MSH/MSH.2")); + assertEquals("^~\\", text(parse("MSH|^~\\"), "/HL7Message/MSH/MSH.2")); + assertEquals("^~\\&", text(parse("MSH|^~\\&"), "/HL7Message/MSH/MSH.2")); + } + + @Test + public void testMessageWithoutHeaderUsesDefaultSeparators() throws Exception { + Document doc = parse("PID|1|A^B~C", true, false); + + assertEquals("1", text(doc, "/HL7Message/PID/PID.1/PID.1.1")); + assertEquals("A", text(doc, "/HL7Message/PID/PID.2[1]/PID.2.1")); + assertEquals("B", text(doc, "/HL7Message/PID/PID.2[1]/PID.2.2")); + assertEquals("C", text(doc, "/HL7Message/PID/PID.2[2]/PID.2.1")); + } + + @Test + public void testSurroundingWhitespaceIsTrimmed() throws Exception { + Document doc = parse("\n MSH|^~\\&|APP\r \n"); + + assertEquals("APP", text(doc, "/HL7Message/MSH/MSH.3/MSH.3.1")); + assertEquals(1, count(doc, "/HL7Message/*")); + } + + @Test + public void testMessageTooShort() throws Exception { + ER7Reader reader = new ER7Reader(false, false, "\r"); + reader.setContentHandler(new org.xml.sax.helpers.DefaultHandler()); + + try { + reader.parse(new InputSource(new StringReader(" MSH| "))); + fail("Expected SAXException"); + } catch (SAXException e) { + assertTrue(e.getMessage().contains("too short")); + } + } + + @Test + public void testContentHandlerRequired() throws Exception { + try { + new ER7Reader(false, false, "\r").parse(new InputSource(new StringReader("MSH|^~\\&"))); + fail("Expected SAXException"); + } catch (SAXException e) { + assertTrue(e.getMessage().contains("ContentHandler")); + } + } +} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTest.java similarity index 70% rename from server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTests.java rename to server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTest.java index aae28422bb..46792bc61f 100644 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTests.java +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTest.java @@ -11,27 +11,19 @@ import java.io.File; -import junit.framework.Assert; - import org.apache.commons.io.FileUtils; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.mirth.connect.model.converters.TestUtil; -public class HL7SerializerTests { +public class HL7SerializerTest { private HL7v2DataTypeProperties defaultProperties; @Before public void setUp() throws Exception { - defaultProperties = new HL7v2DataTypeProperties(); - -// defaultProperties = new Properties(); -// defaultProperties.put("useStrictParser", "false"); -// defaultProperties.put("handleRepetitions", "false"); -// defaultProperties.put("handleSubcomponents", "false"); -// defaultProperties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// defaultProperties.put("outputSegmentDelimiter", "\\r"); + defaultProperties = properties(false, false); } @Test @@ -39,7 +31,7 @@ public void testToXmlDefault() throws Exception { String input = FileUtils.readFileToString(new File("tests/test-hl7-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-output.xml")); ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test @@ -55,7 +47,7 @@ public void testToXmlWhitepsace() throws Exception { String input = FileUtils.readFileToString(new File("tests/test-hl7-whitespace-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-whitespace-output.xml")); ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test @@ -97,12 +89,7 @@ public void testFromXmlMissingSubcomponents() throws Exception { @Test public void testFromXmlSingleSegment() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "false"); -// properties.put("handleSubcomponents", "true"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(false, true); String input = FileUtils.readFileToString(new File("tests/test-hl7-single-segment-input.xml")); String output = FileUtils.readFileToString(new File("tests/test-hl7-single-segment-output.txt")); @@ -120,29 +107,17 @@ public void testFromXmlSingleField() throws Exception { @Test public void testToXmlWithSubcomponents() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "false"); -// properties.put("handleSubcomponents", "true"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(false, true); String input = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-output.xml")); ER7Serializer serializer = new ER7Serializer(properties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test public void testFromXmlWithSubcomponents() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "false"); -// properties.put("handleSubcomponents", "true"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(false, true); String input = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-output.xml")); String output = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-input.txt")); @@ -152,29 +127,17 @@ public void testFromXmlWithSubcomponents() throws Exception { @Test public void testToXmlWithRepetitions() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "true"); -// properties.put("handleSubcomponents", "false"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(true, false); String input = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-output.xml")); ER7Serializer serializer = new ER7Serializer(properties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test public void testFromXmlWithRepetitions() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "true"); -// properties.put("handleSubcomponents", "false"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(true, false); String input = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-output.xml")); String output = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-input.txt")); @@ -187,7 +150,7 @@ public void testToXmlWithBatch() throws Exception { String input = FileUtils.readFileToString(new File("tests/test-hl7-batch-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-batch-output.xml")); ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test @@ -197,4 +160,20 @@ public void testFromXmlWithBatch() throws Exception { ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); Assert.assertEquals(output, TestUtil.convertCRToCRLF(serializer.fromXML(input))); } + + private static void assertXmlEquals(String expected, String actual) throws Exception { + Assert.assertEquals(TestUtil.normalizeLineEndings(expected), TestUtil.normalizeLineEndings(TestUtil.prettyPrintXml(actual))); + } + + /** + * The fixtures predate the typed property classes, which default both flags to true; the + * combination each test was written against survives only in its setup. + */ + private static HL7v2DataTypeProperties properties(boolean handleRepetitions, boolean handleSubcomponents) { + HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); + HL7v2SerializationProperties serializationProperties = (HL7v2SerializationProperties) properties.getSerializationProperties(); + serializationProperties.setHandleRepetitions(handleRepetitions); + serializationProperties.setHandleSubcomponents(handleSubcomponents); + return properties; + } } diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7Tests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7Tests.java deleted file mode 100644 index 343d494e9c..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7Tests.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.hl7v2; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.w3c.dom.Document; -import org.xml.sax.SAXException; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.DocumentSerializer; -import com.mirth.connect.model.converters.Stopwatch; - -public class HL7Tests { - - public static void main(String[] args) { - - String testMessage = ""; - String testXML = null; - try { - testMessage = new String(getBytesFromFile(new File(args[0]))); - if (args.length > 1) { - testXML = new String(getBytesFromFile(new File(args[1]))); - } - System.out.println(testMessage); - } catch (IOException e) { - e.printStackTrace(); - } - try { - - long totalExecutionTime = 0; - int iterations = 1; - for (int i = 0; i < iterations; i++) { - totalExecutionTime += runTest(testMessage, testXML); - } - - System.out.println("Execution time average: " + totalExecutionTime / iterations + " ms"); - } - // System.out.println(new X12Serializer().serialize("SEG*1*2**4*5")); - catch (SAXException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - private static long runTest(String testMessage, String xml) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "true"); -// properties.put("inputSegmentDelimiter", "\r\n|\r|\n"); -// properties.put("outputSegmentDelimiter", "\r"); - stopwatch.start(); - ER7Serializer serializer = new ER7Serializer(null); - String xmloutput = xml; - String er7 = ""; - if (xml == null) { - xmloutput = serializer.toXML(testMessage); - er7 = serializer.fromXML(xmloutput); - stopwatch.stop(); - } else { - - DocumentSerializer docser = new DocumentSerializer(); - Document doc = docser.fromXML(xmloutput); - er7 = serializer.fromXML(docser.toXML(doc)); - stopwatch.stop(); - } - - //System.out.println(xmloutput); - DocumentSerializer docser = new DocumentSerializer(); - Document doc = docser.fromXML(xmloutput); - - System.out.println(docser.toXML(doc)); - System.out.println(er7); - if (er7.trim().equals(testMessage.trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = er7.replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("\nTest Failed!"); - } - return stopwatch.toValue(); - } - - // Returns the contents of the file in a byte array. - private static byte[] getBytesFromFile(File file) throws IOException { - InputStream is = new FileInputStream(file); - - // Get the size of the file - long length = file.length(); - - // You cannot create an array using a long type. - // It needs to be an int type. - // Before converting to an int type, check - // to ensure that file is not larger than Integer.MAX_VALUE. - if (length > Integer.MAX_VALUE) { - // File is too large - } - - // Create the byte array to hold the data - byte[] bytes = new byte[(int) length]; - - // Read in the bytes - int offset = 0; - int numRead = 0; - while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - - // Ensure all the bytes have been read in - if (offset < bytes.length) { - throw new IOException("Could not completely read file " + file.getName()); - } - - // Close the input stream and return bytes - is.close(); - return bytes; - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7v2ResponseValidatorTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7v2ResponseValidatorTest.java new file mode 100644 index 0000000000..abdacf9740 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7v2ResponseValidatorTest.java @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Nico Piel + +package com.mirth.connect.plugins.datatypes.hl7v2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; + +import org.junit.Before; +import org.junit.Test; + +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.ContentType; +import com.mirth.connect.donkey.model.message.MessageContent; +import com.mirth.connect.donkey.model.message.Response; +import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.plugins.datatypes.hl7v2.HL7v2ResponseValidationProperties.OriginalMessageControlId; + +public class HL7v2ResponseValidatorTest { + + private static final String ORIGINAL_ER7 = "MSH|^~\\&|SEND|FAC|RECV|FAC|20261031120000||ADT^A01|CTRL123|P|2.5\rPID|1||12345"; + + private HL7v2SerializationProperties serializationProperties; + private HL7v2ResponseValidationProperties validationProperties; + private ConnectorMessage connectorMessage; + + @Before + public void setup() { + serializationProperties = new HL7v2SerializationProperties(); + validationProperties = new HL7v2ResponseValidationProperties(); + connectorMessage = new ConnectorMessage(); + connectorMessage.setConnectorMap(new HashMap<>()); + connectorMessage.setChannelMap(new HashMap<>()); + setEncoded(ORIGINAL_ER7); + } + + private void setEncoded(String content) { + connectorMessage.setEncoded(new MessageContent("channel", 1, 1, ContentType.ENCODED, content, "HL7V2", false)); + } + + private Response validate(String responseData) { + HL7v2ResponseValidator validator = new HL7v2ResponseValidator(serializationProperties, validationProperties); + return validator.validate(new Response(Status.SENT, responseData), connectorMessage); + } + + private static String ack(String code, String controlId) { + return "MSH|^~\\&|RECV|FAC|SEND|FAC|20261031120001||ACK|ACK1|P|2.5\rMSA|" + code + "|" + controlId; + } + + @Test + public void testBlankResponseIsQueued() { + Response response = validate(" "); + + assertEquals(Status.QUEUED, response.getStatus()); + assertEquals("Empty or blank response received.", response.getStatusMessage()); + } + + @Test + public void testAcceptedAckWithMatchingControlId() { + assertEquals(Status.SENT, validate(ack("AA", "CTRL123")).getStatus()); + } + + @Test + public void testCommitAcceptIsSuccessful() { + assertEquals(Status.SENT, validate(ack("CA", "CTRL123") + "|done").getStatus()); + } + + @Test + public void testAcceptedAckWithMismatchedControlId() { + Response response = validate(ack("AA", "OTHER")); + + assertEquals(Status.ERROR, response.getStatus()); + assertEquals("Message control Ids do not match.", response.getStatusMessage()); + assertTrue(response.getError().contains("Expected: CTRL123")); + assertTrue(response.getError().contains("Actual: OTHER")); + } + + @Test + public void testAcceptedAckWithoutMsa2() { + Response response = validate("MSH|^~\\&|R|F\rMSA|AA"); + + assertEquals(Status.ERROR, response.getStatus()); + assertTrue(response.getError().endsWith("Actual: ")); + } + + @Test + public void testControlIdValidationDisabled() { + validationProperties.setValidateMessageControlId(false); + + assertEquals(Status.SENT, validate(ack("AA", "OTHER")).getStatus()); + } + + @Test + public void testErrorAckWithMsa3AndErr1() { + String responseData = ack("AE", "CTRL123") + "|Something failed|x\rERR|PID^1^3|other"; + Response response = validate(responseData); + + assertEquals(Status.ERROR, response.getStatus()); + assertEquals("NACK sent from receiver. (Error)\nSomething failed\nPID^1^3", response.getStatusMessage()); + assertTrue(response.getError().endsWith("\n\n" + responseData)); + } + + @Test + public void testErrorAckWithTrailingMsa3AndErr1() { + Response response = validate(ack("AE", "CTRL123") + "|Last field\rERR|Only"); + + assertEquals("NACK sent from receiver. (Error)\nLast field\nOnly", response.getStatusMessage()); + } + + @Test + public void testRejectedAckWithoutDetails() { + Response response = validate(ack("AR", "CTRL123")); + + assertEquals(Status.ERROR, response.getStatus()); + assertEquals("NACK sent from receiver. (Rejected)", response.getStatusMessage()); + } + + @Test + public void testCustomAckCodes() { + validationProperties.setSuccessfulACKCode("OK"); + validationProperties.setErrorACKCode("BAD"); + validationProperties.setRejectedACKCode("NO"); + + assertEquals(Status.SENT, validate(ack("OK", "CTRL123")).getStatus()); + assertEquals("NACK sent from receiver. (Error)", validate(ack("BAD", "CTRL123")).getStatusMessage()); + assertEquals("NACK sent from receiver. (Rejected)", validate(ack("NO", "CTRL123")).getStatusMessage()); + assertEquals(Status.QUEUED, validate(ack("AA", "CTRL123")).getStatus()); + } + + @Test + public void testUnknownAckCodeIsInvalid() { + Response response = validate(ack("XX", "CTRL123")); + + assertEquals(Status.QUEUED, response.getStatus()); + assertEquals("Invalid HL7 v2.x acknowledgement received.", response.getStatusMessage()); + } + + @Test + public void testMissingMsaIsInvalid() { + assertEquals("Invalid HL7 v2.x acknowledgement received.", validate("MSH|^~\\&|R|F\rPID|1").getStatusMessage()); + } + + @Test + public void testTruncatedMsaIsInvalid() { + assertEquals("Invalid HL7 v2.x acknowledgement received.", validate("MSH|^~\\&|R|F\rMSA").getStatusMessage()); + } + + @Test + public void testLineBreaksAreConverted() { + String responseData = ack("AA", "CTRL123").replace("\r", "\r\n"); + assertEquals(Status.SENT, validate(responseData).getStatus()); + + serializationProperties.setConvertLineBreaks(false); + assertEquals(Status.SENT, validate(responseData.replace("\r\n", "\r")).getStatus()); + assertEquals(Status.QUEUED, validate(responseData.replace("\r\n", "\n")).getStatus()); + } + + @Test + public void testOriginalControlIdFromXmlEncodedMessage() { + setEncoded("XMLCTRL"); + + assertEquals(Status.SENT, validate(ack("AA", "XMLCTRL")).getStatus()); + } + + @Test + public void testOriginalMessageWithoutControlId() { + setEncoded("MSH|^~\\&|SEND|FAC\rPID|1|2|3|4|5|6|7|8|9|10"); + + assertEquals(Status.SENT, validate("MSH|^~\\&|R|F\rMSA|AA|").getStatus()); + assertEquals(Status.ERROR, validate(ack("AA", "1")).getStatus()); + } + + @Test + public void testOriginalMessageEndingAtControlId() { + setEncoded("MSH|^~\\&|SEND|FAC|RECV|FAC|20261031120000||ADT^A01|LASTCTRL"); + + assertEquals(Status.SENT, validate(ack("AA", "LASTCTRL")).getStatus()); + } + + @Test + public void testOriginalControlIdFromConnectorMap() { + validationProperties.setOriginalMessageControlId(OriginalMessageControlId.Map_Variable); + validationProperties.setOriginalIdMapVariable("controlId"); + connectorMessage.getConnectorMap().put("controlId", "FROMMAP"); + connectorMessage.getChannelMap().put("controlId", "IGNORED"); + + assertEquals(Status.SENT, validate(ack("AA", "FROMMAP")).getStatus()); + } + + @Test + public void testOriginalControlIdFromChannelMap() { + validationProperties.setOriginalMessageControlId(OriginalMessageControlId.Map_Variable); + validationProperties.setOriginalIdMapVariable("controlId"); + connectorMessage.getChannelMap().put("controlId", 42); + + assertEquals(Status.SENT, validate(ack("AA", "42")).getStatus()); + } + + @Test + public void testOriginalControlIdMapVariableNotSet() { + validationProperties.setOriginalMessageControlId(OriginalMessageControlId.Map_Variable); + validationProperties.setOriginalIdMapVariable(""); + + Response response = validate(ack("AA", "CTRL123")); + assertEquals(Status.QUEUED, response.getStatus()); + assertEquals("Error validating response: Map variable for original control Id not set.", response.getStatusMessage()); + + validationProperties.setOriginalIdMapVariable("missing"); + assertEquals("Error validating response: Map variable for original control Id not set.", validate(ack("AA", "CTRL123")).getStatusMessage()); + } + + @Test + public void testXmlAcceptedAck() { + String xml = " AA CTRL123"; + assertEquals(Status.SENT, validate(xml).getStatus()); + + validationProperties.setValidateMessageControlId(false); + assertEquals(Status.SENT, validate(xml.replace("CTRL123", "OTHER")).getStatus()); + } + + @Test + public void testXmlAcceptedAckWithMismatchedControlId() { + Response response = validate("AAOTHER"); + + assertEquals(Status.ERROR, response.getStatus()); + assertEquals("Message control Ids do not match.", response.getStatusMessage()); + } + + @Test + public void testXmlErrorAndRejectedAcks() { + String xml = "AEBad thingPID"; + assertEquals("NACK sent from receiver. (Error)\nBad thing\nPID", validate(xml).getStatusMessage()); + + assertEquals("NACK sent from receiver. (Rejected)", validate("CR").getStatusMessage()); + } + + /* + * Characterizes existing behaviour: an unknown ACK code in an ER7 response is QUEUED as invalid + * (see testUnknownAckCodeIsInvalid), but in an XML response the status is left as it was. + */ + @Test + public void testXmlUnknownAckCodeLeavesStatusUnchanged() { + assertEquals(Status.SENT, validate("XX").getStatus()); + } + + @Test + public void testXmlWithDoctypeIsRejected() { + Response response = validate("]>&xxe;"); + + assertEquals(Status.QUEUED, response.getStatus()); + assertTrue(response.getStatusMessage().startsWith("Error validating response: ")); + assertTrue(response.getStatusMessage().contains("DOCTYPE")); + } +} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPReaderTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPReaderTest.java new file mode 100644 index 0000000000..42c2b76cef --- /dev/null +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPReaderTest.java @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Nico Piel + +package com.mirth.connect.plugins.datatypes.ncpdp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import com.mirth.connect.model.converters.XMLPrettyPrinter; + +public class NCPDPReaderTest { + + private static final String SEGMENT = "\u001E"; + private static final String GROUP = "\u001D"; + private static final String FIELD = "\u001C"; + + // BIN, version, transaction code, PCN, count, provider id qualifier, provider id, date of service, vendor id + private static final String REQUEST_HEADER_D0 = "610066" + "D0" + "B1" + "PCN1234567" + "1" + "01" + "123456789012345" + "20261031" + "VENDOR1234"; + // Version, transaction code, count, status, provider id qualifier, provider id, date of service + private static final String RESPONSE_HEADER_D0 = "D0" + "B1" + "1" + "A" + "01" + "123456789012345" + "20261031"; + + private static Document parse(String message) throws Exception { + NCPDPReader reader = new NCPDPReader(SEGMENT, GROUP, FIELD); + StringWriter writer = new StringWriter(); + XMLPrettyPrinter printer = new XMLPrettyPrinter(writer); + printer.setEncodeEntities(true); + reader.setContentHandler(printer); + reader.parse(new InputSource(new StringReader(message))); + return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(writer.toString()))); + } + + private static String segment(String id, String... fields) { + StringBuilder builder = new StringBuilder(SEGMENT).append(FIELD).append(id); + for (String field : fields) { + builder.append(FIELD).append(field); + } + return builder.toString(); + } + + private static String text(Document doc, String xpath) throws Exception { + return XPathFactory.newInstance().newXPath().evaluate(xpath, doc); + } + + private static int count(Document doc, String xpath) throws Exception { + return ((NodeList) XPathFactory.newInstance().newXPath().evaluate(xpath, doc, XPathConstants.NODESET)).getLength(); + } + + @Test + public void testUnknownTransactionCodeAndField() throws Exception { + String header = REQUEST_HEADER_D0.substring(0, 8) + "ZZ" + REQUEST_HEADER_D0.substring(10); + Document doc = parse(header + segment("AM01", "Q9value")); + + assertEquals("NCPDP_D0_ZZ_Request", doc.getDocumentElement().getNodeName()); + assertEquals("value", text(doc, "/NCPDP_D0_ZZ_Request/Patient/Q9_field")); + } + + @Test + public void testTransactionGroups() throws Exception { + String message = REQUEST_HEADER_D0 + + segment("AM04", "C2CARD1") + + GROUP + segment("AM07", "D2RX1", "E703") + + GROUP + segment("AM07", "D2RX2", "E704"); + Document doc = parse(message); + + String root = "/NCPDP_D0_Billing_Request"; + assertEquals("CARD1", text(doc, root + "/Insurance/CardholderId")); + assertEquals(1, count(doc, root + "/TRANSACTIONS")); + assertEquals(2, count(doc, root + "/TRANSACTIONS/TRANSACTION")); + assertEquals("1", text(doc, root + "/TRANSACTIONS/TRANSACTION[1]/@counter")); + assertEquals("RX1", text(doc, root + "/TRANSACTIONS/TRANSACTION[1]/Claim/PrescriptionServiceReferenceNumber")); + assertEquals("03", text(doc, root + "/TRANSACTIONS/TRANSACTION[1]/Claim/QuantityDispensed")); + assertEquals("2", text(doc, root + "/TRANSACTIONS/TRANSACTION[2]/@counter")); + assertEquals("RX2", text(doc, root + "/TRANSACTIONS/TRANSACTION[2]/Claim/PrescriptionServiceReferenceNumber")); + } + + @Test + public void testCountFieldWrapsRepeatingFields() throws Exception { + Document doc = parse(RESPONSE_HEADER_D0 + segment("AM21", "ANR", "FA2", "FB70", "FB75", "F3AUTH1")); + + String status = "/NCPDP_D0_Billing_Response/ResponseStatus"; + assertEquals("R", text(doc, status + "/TransactionResponseStatus")); + assertEquals("2", text(doc, status + "/RejectCount/@RejectCount")); + assertEquals(2, count(doc, status + "/RejectCount/RejectCode")); + assertEquals("70", text(doc, status + "/RejectCount/RejectCode[1]")); + assertEquals("75", text(doc, status + "/RejectCount/RejectCode[2]")); + // The first non-repeating field closes the count element + assertEquals("AUTH1", text(doc, status + "/AuthorizationNumber")); + assertEquals(0, count(doc, status + "/RejectCount/AuthorizationNumber")); + } + + @Test + public void testCountFieldClosedAtEndOfSegment() throws Exception { + Document doc = parse(RESPONSE_HEADER_D0 + segment("AM21", "FA1", "FB70")); + + assertEquals("70", text(doc, "/NCPDP_D0_Billing_Response/ResponseStatus/RejectCount/RejectCode")); + } + + @Test + public void testCounterFieldsGroupFollowingFields() throws Exception { + Document doc = parse(REQUEST_HEADER_D0 + segment("AM08", "7E1", "E5M0", "7E2", "E5P0")); + + String dur = "/NCPDP_D0_Billing_Request/DURPPS"; + assertEquals(2, count(doc, dur + "/DurPpsCodeCounter")); + assertEquals("1", text(doc, dur + "/DurPpsCodeCounter[1]/@counter")); + assertEquals("M0", text(doc, dur + "/DurPpsCodeCounter[1]/ProfessionalServiceCode")); + assertEquals("2", text(doc, dur + "/DurPpsCodeCounter[2]/@counter")); + assertEquals("P0", text(doc, dur + "/DurPpsCodeCounter[2]/ProfessionalServiceCode")); + } + + @Test + public void testFieldNamesDependOnVersion() throws Exception { + String segment = segment("AM07", "NX2", "DK1", "DK2"); + + // NX is a count field only in D0 + Document d0 = parse(REQUEST_HEADER_D0 + segment); + assertEquals("2", text(d0, "/NCPDP_D0_Billing_Request/Claim/SubmissionClarificationCodeCount/@SubmissionClarificationCodeCount")); + assertEquals(2, count(d0, "/NCPDP_D0_Billing_Request/Claim/SubmissionClarificationCodeCount/SubmissionClarificationCode")); + + String header51 = REQUEST_HEADER_D0.substring(0, 6) + "51" + REQUEST_HEADER_D0.substring(8); + Document v51 = parse(header51 + segment); + assertEquals("NCPDP_51_Billing_Request", v51.getDocumentElement().getNodeName()); + assertEquals("2", text(v51, "/NCPDP_51_Billing_Request/Claim/NX_field")); + assertEquals(2, count(v51, "/NCPDP_51_Billing_Request/Claim/SubmissionClarificationCode")); + } + + @Test + public void testSegmentWithoutFields() throws Exception { + Document doc = parse(REQUEST_HEADER_D0 + segment("AM01", "C51") + SEGMENT + "AM99"); + + assertEquals(1, count(doc, "/NCPDP_D0_Billing_Request/AM99")); + assertEquals(0, count(doc, "/NCPDP_D0_Billing_Request/AM99/*")); + } + + @Test + public void testMessageTooShort() throws Exception { + NCPDPReader reader = new NCPDPReader(SEGMENT, GROUP, FIELD); + reader.setContentHandler(new DefaultHandler()); + + try { + reader.parse(new InputSource(new StringReader(" AB "))); + fail("Expected SAXException"); + } catch (SAXException e) { + assertTrue(e.getMessage().contains("too short")); + } + } +} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java index a3c84faa98..2146b3de60 100644 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java @@ -1,14 +1,21 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2017 Mirth Corporation +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan package com.mirth.connect.plugins.datatypes.ncpdp; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.File; + +import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Test; import org.xml.sax.SAXParseException; import com.mirth.connect.donkey.model.message.MessageSerializerException; +import com.mirth.connect.model.converters.TestUtil; import com.mirth.connect.model.datatype.SerializerProperties; public class NCPDPSerializerTest { @@ -41,6 +48,89 @@ public void testFromXMLWithExternalDtd() { assertTrue(exceptionCaught); } + @Test + public void test51RequestToXml() throws Exception { + String input = read("test-ncpdp-51-request-input.txt"); + String output = read("test-ncpdp-51-request-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void test51RequestFromXml() throws Exception { + String input = read("test-ncpdp-51-request-output.xml"); + String output = read("test-ncpdp-51-request-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void test51ResponseToXml() throws Exception { + String input = read("test-ncpdp-51-response-input.txt"); + String output = read("test-ncpdp-51-response-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void test51ResponseFromXml() throws Exception { + String input = read("test-ncpdp-51-response-output.xml"); + String output = read("test-ncpdp-51-response-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void testD0RequestToXml() throws Exception { + String input = read("test-ncpdp-d0-request-input.txt"); + String output = read("test-ncpdp-d0-request-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void testD0RequestFromXml() throws Exception { + String input = read("test-ncpdp-d0-request-output.xml"); + String output = read("test-ncpdp-d0-request-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void testD0ResponseToXml() throws Exception { + String input = read("test-ncpdp-d0-response-input.txt"); + String output = read("test-ncpdp-d0-response-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void testD0ResponseFromXml() throws Exception { + String input = read("test-ncpdp-d0-response-output.xml"); + String output = read("test-ncpdp-d0-response-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void testvalidateTransformHeaderWithfieldValue() throws Exception { + String input = "6100"; + String expectedOutput = "6100 "; + String actualOutput = serializer.validateTransformHeader(input); + assertEquals(actualOutput, expectedOutput); + } + + @Test + public void testvalidateTransformHeaderWithoutFieldValue() throws Exception { + String input = ""; + String expectedOutput = " "; + String actualOutput = serializer.validateTransformHeader(input); + assertEquals(actualOutput, expectedOutput); + } + + @Test + public void testvalidateTransformHeaderWithOneEndTag() throws Exception { + String input = ""; + String expectedOutput = " "; + String actualOutput = serializer.validateTransformHeader(input); + assertEquals(actualOutput, expectedOutput); + } + private static String read(String name) throws Exception { + return FileUtils.readFileToString(new File("tests/" + name)); + } + private static String validXml = "\r\n" + "bar"; diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTests.java deleted file mode 100644 index 99496d723a..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTests.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.ncpdp; - -import java.io.File; - -import junit.framework.Assert; - -import org.apache.commons.io.FileUtils; -import org.junit.Before; -import org.junit.Test; - -import com.mirth.connect.model.converters.TestUtil; - -public class NCPDPSerializerTests { - private NCPDPDataTypeProperties defaultProperties; - - @Before - public void setUp() throws Exception { - defaultProperties = new NCPDPDataTypeProperties(); -// defaultProperties = new Properties(); -// defaultProperties.put("segmentDelimiter", "0x1E"); -// defaultProperties.put("groupDelimiter", "0x1D"); -// defaultProperties.put("fieldDelimiter", "0x1C"); -// defaultProperties.put("useStrictValidation", "false"); - } - - @Test - public void test51RequestToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-input.txt")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-output.xml")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void test51RequestFromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-output.xml")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-input.txt")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void test51ResponseToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-input.txt")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-output.xml")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void test51ResponseFromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-output.xml")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-input.txt")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void testD0ToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-input.txt")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-output.xml")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void testD0FromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-output.xml")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-input.txt")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void testvalidateTransformHeaderWithfieldValue() throws Exception { - String input = "6100"; - String expectedOutput = "6100 "; - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - String actualOutput = serializer.validateTransformHeader(input); - Assert.assertEquals(actualOutput, expectedOutput); - } - - @Test - public void testvalidateTransformHeaderWithoutFieldValue() throws Exception { - String input = ""; - String expectedOutput = " "; - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - String actualOutput = serializer.validateTransformHeader(input); - Assert.assertEquals(actualOutput, expectedOutput); - } - - @Test - public void testvalidateTransformHeaderWithOneEndTag() throws Exception { - String input = ""; - String expectedOutput = " "; - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - String actualOutput = serializer.validateTransformHeader(input); - Assert.assertEquals(actualOutput, expectedOutput); - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPTests.java deleted file mode 100644 index 3890a4dd57..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPTests.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.ncpdp; - -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; - -import org.apache.commons.io.FileUtils; -import org.w3c.dom.Document; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.DocumentSerializer; -import com.mirth.connect.model.converters.Stopwatch; - -public class NCPDPTests { - public static void main(String[] args) throws Exception { - String testMessage = ""; - ArrayList testFiles = new ArrayList(); - testFiles.add("C:\\NCPDP_51_B1_Request.txt"); - testFiles.add("C:\\NCPDP_51_B1_Request_v2.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v4.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v5.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v6.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v7.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v8.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v9.txt"); - testFiles.add("C:\\NCPDP_51_B2_Request.txt"); - testFiles.add("C:\\NCPDP_51_B2_Request_v2.txt"); - testFiles.add("C:\\NCPDP_51_B2_Response.txt"); - testFiles.add("C:\\NCPDP_51_B2_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_B2_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_B3_Request.txt"); - testFiles.add("C:\\NCPDP_51_B3_Response.txt"); - testFiles.add("C:\\NCPDP_51_B3_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_B3_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_E1_Request.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v4.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v5.txt"); - testFiles.add("C:\\NCPDP_51_N1_Request.txt"); - testFiles.add("C:\\NCPDP_51_N2_Request.txt"); - testFiles.add("C:\\NCPDP_51_P1_Request.txt"); - testFiles.add("C:\\NCPDP_51_P1_Response.txt"); - testFiles.add("C:\\NCPDP_51_P1_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_P1_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_P2_Request.txt"); - testFiles.add("C:\\NCPDP_51_P2_Response.txt"); - testFiles.add("C:\\NCPDP_51_P3_Request.txt"); - testFiles.add("C:\\NCPDP_51_P3_Response.txt"); - testFiles.add("C:\\NCPDP_51_P3_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_P4_Request.txt"); - testFiles.add("C:\\NCPDP_51_P4_Response.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_1.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_2.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_3.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_4.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_5.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_6.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_7.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_8.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_9.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_10.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_11.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_12.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_13.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_14.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_15.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_16.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_17.txt"); - - for (String testFile : testFiles) { - testMessage = new String(FileUtils.readFileToByteArray(new File(testFile))); - System.out.println("Processing test file:" + testFile); - - try { - long totalExecutionTime = 0; - int iterations = 1; - for (int i = 0; i < iterations; i++) { - totalExecutionTime += runTest(testMessage); - } - - //System.out.println("Execution time average: " + totalExecutionTime/iterations + " ms"); - } - // System.out.println(new X12Serializer().serialize("SEG*1*2**4*5")); - catch (SAXException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private static long runTest(String testMessage) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); -// Properties properties = new Properties(); - String SchemaUrl = "/ncpdp51.xsd"; -// properties.put("useStrictParser", "true"); -// properties.put("http://java.sun.com/xml/jaxp/properties/schemaSource",SchemaUrl); - stopwatch.start(); - NCPDPSerializer serializer = new NCPDPSerializer(null); - String xmloutput = serializer.toXML(testMessage); - //System.out.println(xmloutput); - DocumentSerializer docser = new DocumentSerializer(); - Document doc = docser.fromXML(xmloutput); - XMLReader xr = XMLReaderFactory.createXMLReader(); - - NCPDPXMLHandler handler = new NCPDPXMLHandler("\u001E", "\u001D", "\u001C", "51"); - - xr.setContentHandler(handler); - xr.setErrorHandler(handler); - xr.setFeature("http://xml.org/sax/features/validation", true); - xr.setFeature("http://apache.org/xml/features/validation/schema", true); - xr.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true); - xr.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - xr.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema"); - xr.setProperty("http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation", SchemaUrl); - xr.setProperty("http://java.sun.com/xml/jaxp/properties/schemaSource", "/ncpdp51.xsd"); - xr.parse(new InputSource(new StringReader(xmloutput))); - stopwatch.stop(); - - //System.out.println(docser.serialize(doc)); //handler.getOutput()); - //System.out.println(handler.getOutput()); - //System.out.println(xmloutput); - if (handler.getOutput().toString().replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = handler.getOutput().toString().replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("Test Failed!"); - } - return stopwatch.toValue(); - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/httpauth/digest/DigestAuthenticatorTest.java b/server/src/test/java/com/mirth/connect/plugins/httpauth/digest/DigestAuthenticatorTest.java index 48440a01ba..41283f4715 100644 --- a/server/src/test/java/com/mirth/connect/plugins/httpauth/digest/DigestAuthenticatorTest.java +++ b/server/src/test/java/com/mirth/connect/plugins/httpauth/digest/DigestAuthenticatorTest.java @@ -1,22 +1,43 @@ package com.mirth.connect.plugins.httpauth.digest; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.codec.binary.Hex; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.server.channel.Channel; import com.mirth.connect.donkey.server.channel.Connector; import com.mirth.connect.donkey.util.MessageMaps; +import com.mirth.connect.plugins.httpauth.AuthenticationResult; +import com.mirth.connect.plugins.httpauth.AuthenticationResult.AuthStatus; +import com.mirth.connect.plugins.httpauth.RequestInfo; +import com.mirth.connect.plugins.httpauth.digest.DigestHttpAuthProperties.Algorithm; +import com.mirth.connect.plugins.httpauth.digest.DigestHttpAuthProperties.QOPMode; public class DigestAuthenticatorTest { + private static final String REALM = "My Realm"; + private static final String OPAQUE = "opaque-value"; + private static final String URI = "/api/test"; + private static final String USER = "alice"; + private static final String PASSWORD = "secret"; + DigestAuthenticator authenticator; Connector connector; DigestAuthenticatorProvider provider; @@ -26,12 +47,261 @@ public class DigestAuthenticatorTest { @Before public void setup() { connector = Mockito.mock(Connector.class); + Channel channel = Mockito.mock(Channel.class); + doReturn("channelId").when(connector).getChannelId(); + doReturn(channel).when(connector).getChannel(); + doReturn("channelName").when(channel).getName(); provider = Mockito.mock(DigestAuthenticatorProvider.class); doReturn(connector).when(provider).getConnector(); messageMap = new CustomMessageMap(); messageMap.map = new HashMap<>(); authenticator = new DigestAuthenticator(provider, messageMap); props = new DigestHttpAuthProperties(); + props.setRealm(REALM); + props.setOpaque(OPAQUE); + props.setCredentialsMap(Collections.singletonMap(USER, PASSWORD)); + doReturn(props).when(provider).getProperties(); + } + + private static RequestInfo request(String uri, String authorization, byte[] entity) { + Map> headers = new HashMap<>(); + if (authorization != null) { + headers.put("Authorization", Collections.singletonList(authorization)); + } + return new RequestInfo("127.0.0.1", 50000, "127.0.0.1", 8080, "HTTP/1.1", "POST", uri, headers, new HashMap<>(), () -> entity); + } + + private static String md5(String... parts) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(String.join(":", parts).getBytes(StandardCharsets.ISO_8859_1)); + return Hex.encodeHexString(md.digest()); + } + + private static String directive(AuthenticationResult result, String name) { + String header = result.getResponseHeaders().get("WWW-Authenticate").get(0); + Matcher matcher = Pattern.compile("\\b" + name + "=\"([^\"]*)\"").matcher(header); + return matcher.find() ? matcher.group(1) : null; + } + + private String challengeNonce() { + AuthenticationResult challenge = authenticator.authenticate(request(URI, null, null)); + assertEquals(AuthStatus.CHALLENGED, challenge.getStatus()); + return directive(challenge, "nonce"); + } + + private static String authorization(String username, String realm, String nonce, String uri, String response, String opaque, String extra) { + return "Digest username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\"" + response + "\", opaque=\"" + opaque + "\"" + (extra == null ? "" : ", " + extra); + } + + private AuthenticationResult authenticateWithQop(String nonce, String nc) throws Exception { + String ha1 = md5(USER, REALM, PASSWORD); + String ha2 = md5("POST", URI); + String response = md5(ha1, nonce, nc, "cnonce1", "auth", ha2); + return authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, response, OPAQUE, "qop=auth, nc=" + nc + ", cnonce=\"cnonce1\""), null)); + } + + @Test + public void testChallengeWithoutAuthorizationHeader() { + AuthenticationResult result = authenticator.authenticate(request(URI + "?a=b", null, null)); + + assertEquals(AuthStatus.CHALLENGED, result.getStatus()); + assertTrue(result.getResponseHeaders().get("WWW-Authenticate").get(0).startsWith("Digest ")); + assertEquals(REALM, directive(result, "realm")); + assertEquals(URI, directive(result, "domain")); + assertEquals("MD5,MD5-sess", directive(result, "algorithm")); + assertEquals("auth,auth-int", directive(result, "qop")); + assertEquals(OPAQUE, directive(result, "opaque")); + assertFalse(directive(result, "nonce").isEmpty()); + assertEquals(null, directive(result, "stale")); + } + + @Test + public void testChallengeOmitsEmptyQopAndOpaque() { + props.setQopModes(EnumSet.noneOf(QOPMode.class)); + props.setOpaque(""); + AuthenticationResult result = authenticator.authenticate(request(URI, null, null)); + + assertEquals(null, directive(result, "qop")); + assertEquals(null, directive(result, "opaque")); + } + + @Test + public void testSuccessWithoutQop() throws Exception { + String nonce = challengeNonce(); + String response = md5(md5(USER, REALM, PASSWORD), nonce, md5("POST", URI)); + + AuthenticationResult result = authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, response, OPAQUE, null), null)); + + assertEquals(AuthStatus.SUCCESS, result.getStatus()); + assertEquals(USER, result.getUsername()); + assertEquals(REALM, result.getRealm()); + } + + @Test + public void testSuccessWithQopAuth() throws Exception { + assertEquals(AuthStatus.SUCCESS, authenticateWithQop(challengeNonce(), "00000001").getStatus()); + } + + @Test + public void testSuccessWithMd5Sess() throws Exception { + String nonce = challengeNonce(); + String ha1 = md5(md5(USER, REALM, PASSWORD), nonce, "cnonce1"); + String response = md5(ha1, nonce, "00000001", "cnonce1", "auth", md5("POST", URI)); + String extra = "algorithm=MD5-sess, qop=auth, nc=00000001, cnonce=\"cnonce1\""; + + AuthenticationResult result = authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, response, OPAQUE, extra), null)); + + assertEquals(AuthStatus.SUCCESS, result.getStatus()); + } + + @Test + public void testSuccessWithQopAuthInt() throws Exception { + String nonce = challengeNonce(); + byte[] body = "hello".getBytes(StandardCharsets.UTF_8); + String ha2 = md5("POST", URI, md5("hello")); + String response = md5(md5(USER, REALM, PASSWORD), nonce, "00000001", "cnonce1", "auth-int", ha2); + String extra = "qop=auth-int, nc=00000001, cnonce=\"cnonce1\""; + + AuthenticationResult result = authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, response, OPAQUE, extra), body)); + + assertEquals(AuthStatus.SUCCESS, result.getStatus()); + } + + @Test + public void testEmptyUriMatchesRoot() throws Exception { + String nonce = challengeNonce(); + String response = md5(md5(USER, REALM, PASSWORD), nonce, md5("POST", "")); + + AuthenticationResult result = authenticator.authenticate(request("/", authorization(USER, REALM, nonce, "", response, OPAQUE, null), null)); + + assertEquals(AuthStatus.SUCCESS, result.getStatus()); + } + + @Test + public void testWrongPasswordIsChallengedWithoutStale() throws Exception { + String nonce = challengeNonce(); + String response = md5(md5(USER, REALM, "wrong"), nonce, md5("POST", URI)); + + AuthenticationResult result = authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, response, OPAQUE, null), null)); + + assertEquals(AuthStatus.CHALLENGED, result.getStatus()); + assertEquals(null, directive(result, "stale")); + } + + @Test + public void testUnknownNonceWithValidDigestIsStale() throws Exception { + String nonce = "unknown-nonce"; + String response = md5(md5(USER, REALM, PASSWORD), nonce, md5("POST", URI)); + + AuthenticationResult result = authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, response, "", null), null)); + + assertEquals(AuthStatus.CHALLENGED, result.getStatus()); + assertEquals("true", directive(result, "stale")); + assertFalse(nonce.equals(directive(result, "nonce"))); + } + + @Test + public void testReplayedNonceCountIsRejected() throws Exception { + String nonce = challengeNonce(); + + assertEquals(AuthStatus.SUCCESS, authenticateWithQop(nonce, "00000002").getStatus()); + assertEquals(AuthStatus.CHALLENGED, authenticateWithQop(nonce, "00000002").getStatus()); + assertEquals(AuthStatus.CHALLENGED, authenticateWithQop(nonce, "00000001").getStatus()); + assertEquals(AuthStatus.SUCCESS, authenticateWithQop(nonce, "00000003").getStatus()); + } + + @Test + public void testNonceCountAboveMaximumIsStale() throws Exception { + AuthenticationResult result = authenticateWithQop(challengeNonce(), "00000401"); + + assertEquals(AuthStatus.CHALLENGED, result.getStatus()); + assertEquals("true", directive(result, "stale")); + } + + @Test + public void testMalformedNonceCountIsRejected() throws Exception { + AuthenticationResult result = authenticateWithQop(challengeNonce(), "zz"); + + assertEquals(AuthStatus.CHALLENGED, result.getStatus()); + assertEquals(null, directive(result, "stale")); + } + + /* + * Characterizes existing behaviour, not an endorsement: without qop the request follows RFC 2069, + * which has no nonce count, so a nonce stays reusable until it expires. + */ + @Test + public void testNonceWithoutCountCanBeReused() throws Exception { + String nonce = challengeNonce(); + String response = md5(md5(USER, REALM, PASSWORD), nonce, md5("POST", URI)); + String authorization = authorization(USER, REALM, nonce, URI, response, OPAQUE, null); + + assertEquals(AuthStatus.SUCCESS, authenticator.authenticate(request(URI, authorization, null)).getStatus()); + assertEquals(AuthStatus.SUCCESS, authenticator.authenticate(request(URI, authorization, null)).getStatus()); + } + + @Test + public void testInvalidDirectivesAreChallenged() throws Exception { + String nonce = challengeNonce(); + String ha1 = md5(USER, REALM, PASSWORD); + String valid = md5(ha1, nonce, md5("POST", URI)); + + String[] authorizations = new String[] { + // missing username, realm, uri, response + "Digest realm=\"" + REALM + "\", nonce=\"" + nonce + "\", uri=\"" + URI + "\", response=\"" + valid + "\"", + "Digest username=\"" + USER + "\", nonce=\"" + nonce + "\", uri=\"" + URI + "\", response=\"" + valid + "\"", + "Digest username=\"" + USER + "\", realm=\"" + REALM + "\", nonce=\"" + nonce + "\", response=\"" + valid + "\"", + "Digest username=\"" + USER + "\", realm=\"" + REALM + "\", nonce=\"" + nonce + "\", uri=\"" + URI + "\"", + // mismatched realm, uri, opaque + authorization(USER, "Other Realm", nonce, URI, valid, OPAQUE, null), + authorization(USER, REALM, nonce, "/other", valid, OPAQUE, null), + authorization(USER, REALM, nonce, URI, valid, "other-opaque", null), + // unknown user + authorization("bob", REALM, nonce, URI, valid, OPAQUE, null), + // unsupported algorithm and qop + authorization(USER, REALM, nonce, URI, valid, OPAQUE, "algorithm=SHA-256"), + authorization(USER, REALM, nonce, URI, valid, OPAQUE, "qop=bogus, nc=00000001, cnonce=\"c\""), + // qop requires nc and cnonce + authorization(USER, REALM, nonce, URI, valid, OPAQUE, "qop=auth, cnonce=\"c\""), + authorization(USER, REALM, nonce, URI, valid, OPAQUE, "qop=auth, nc=00000005"), + // MD5-sess requires cnonce + authorization(USER, REALM, nonce, URI, valid, OPAQUE, "algorithm=MD5-sess"), + // no nonce at all + "Digest username=\"" + USER + "\", realm=\"" + REALM + "\", uri=\"" + URI + "\", response=\"" + valid + "\"" + }; + + for (String authorization : authorizations) { + AuthenticationResult result = authenticator.authenticate(request(URI, authorization, null)); + assertEquals(authorization, AuthStatus.CHALLENGED, result.getStatus()); + assertEquals(authorization, null, directive(result, "stale")); + } + + // The nonce is still usable after all of the failed attempts + assertEquals(AuthStatus.SUCCESS, authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, valid, OPAQUE, null), null)).getStatus()); + } + + @Test + public void testDisabledAlgorithmAndQopAreRejected() throws Exception { + props.setAlgorithms(EnumSet.of(Algorithm.MD5_SESS)); + props.setQopModes(EnumSet.of(QOPMode.AUTH_INT)); + String ha1 = md5(USER, REALM, PASSWORD); + + // Each attempt gets its own nonce, so a rejection can't come from the nonce count instead + String nonce = challengeNonce(); + String md5Response = md5(ha1, nonce, md5("POST", URI)); + AuthenticationResult result = authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, md5Response, OPAQUE, "algorithm=MD5"), null)); + assertEquals(AuthStatus.CHALLENGED, result.getStatus()); + + assertEquals(AuthStatus.CHALLENGED, authenticateWithQop(challengeNonce(), "00000001").getStatus()); + + // Positive control: the enabled combination still succeeds + nonce = challengeNonce(); + String sessHa1 = md5(ha1, nonce, "cnonce1"); + String ha2 = md5("POST", URI, md5("hello")); + String response = md5(sessHa1, nonce, "00000001", "cnonce1", "auth-int", ha2); + String extra = "algorithm=MD5-sess, qop=auth-int, nc=00000001, cnonce=\"cnonce1\""; + result = authenticator.authenticate(request(URI, authorization(USER, REALM, nonce, URI, response, OPAQUE, extra), "hello".getBytes(StandardCharsets.UTF_8))); + assertEquals(AuthStatus.SUCCESS, result.getStatus()); } @Test diff --git a/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTests.java b/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTest.java similarity index 92% rename from server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTests.java rename to server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTest.java index 0d125317e8..90c2434941 100644 --- a/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTests.java +++ b/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTest.java @@ -13,7 +13,7 @@ import org.junit.Test; -public class Pre22PasswordCheckerTests { +public class Pre22PasswordCheckerTest { @Test public void testCheckPassword() throws Exception { diff --git a/server/src/test/java/com/mirth/connect/util/ValueReplacerTests.java b/server/src/test/java/com/mirth/connect/util/ValueReplacerTest.java similarity index 99% rename from server/src/test/java/com/mirth/connect/util/ValueReplacerTests.java rename to server/src/test/java/com/mirth/connect/util/ValueReplacerTest.java index 2acfadbbf6..1d7a79dc75 100644 --- a/server/src/test/java/com/mirth/connect/util/ValueReplacerTests.java +++ b/server/src/test/java/com/mirth/connect/util/ValueReplacerTest.java @@ -26,7 +26,7 @@ import com.mirth.connect.donkey.model.message.ConnectorMessage; -public class ValueReplacerTests { +public class ValueReplacerTest { private ValueReplacer replacer; private Map> map; private ConnectorMessage connectorMessage; diff --git a/server/tests/dicom/README.md b/server/tests/dicom/README.md new file mode 100644 index 0000000000..d341744787 --- /dev/null +++ b/server/tests/dicom/README.md @@ -0,0 +1,26 @@ +# DICOM test samples + +Each sample is one instance from a public collection of The Cancer Imaging Archive (TCIA), as +distributed by the NCI Imaging Data Commons (IDC) and obtained by way of +https://saga-it.com/dicom/samples. The files are stored as received: OIE has not modified them. Each +collection's license and DOI are in the table below (looked up in IDC by the sample's +SeriesInstanceUID); see the TCIA data usage policy at +https://www.cancerimagingarchive.net/data-usage-policies-and-restrictions/ for citation requirements. + +Each `.dcm` is paired with `.xml`, the expected serialization of its header. +`DICOMSerializerTest` discovers the pairs by scanning this directory, so a new sample needs no code +change. The XML covers the header only - `DICOMSerializer.removePixelData` strips pixel data before +serializing, since including it would dwarf the rest of the document. + +| Sample | Modality | Collection | License | Collection DOI | +| --- | --- | --- | --- | --- | +| `ct-abdomen-c4kc-kits-instance` | CT | C4KC-KiTS | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [10.7937/TCIA.2019.IX49E8NX](https://doi.org/10.7937/TCIA.2019.IX49E8NX) | +| `ct-chest-lidc-idri-instance` | CT | LIDC-IDRI | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [10.7937/K9/TCIA.2015.LO9QL9SX](https://doi.org/10.7937/K9/TCIA.2015.LO9QL9SX) | +| `ct-lung-screening-nlst-instance` | CT | NLST | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | [10.7937/TCIA.HMQ8-J677](https://doi.org/10.7937/TCIA.HMQ8-J677) | +| `mg-mammography-cbis-ddsm-instance` | MG | CBIS-DDSM | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [10.7937/K9/TCIA.2016.7O02S9CY](https://doi.org/10.7937/K9/TCIA.2016.7O02S9CY) | +| `mr-prostate-prostatex-instance` | MR | PROSTATEx | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [10.7937/K9TCIA.2017.MURS5CL](https://doi.org/10.7937/K9TCIA.2017.MURS5CL) | +| `pt-breast-qin-01-instance` | PT | QIN-BREAST | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) | [10.7937/K9/TCIA.2016.21JUEBH0](https://doi.org/10.7937/K9/TCIA.2016.21JUEBH0) | +| `us-lymph-node-cmb-lca-instance` | US | CMB-LCA | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | [10.7937/3CX3-S132](https://doi.org/10.7937/3CX3-S132) | + +The patient identifiers in these files are the collections' own pseudonyms (for example +`LIDC-IDRI-0580`), not real ones. diff --git a/server/tests/dicom/ct-abdomen-c4kc-kits-instance.dcm b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.dcm new file mode 100644 index 0000000000..ad88cdc16f Binary files /dev/null and b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.dcm differ diff --git a/server/tests/dicom/ct-abdomen-c4kc-kits-instance.xml b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.xml new file mode 100644 index 0000000000..98b7c67da5 --- /dev/null +++ b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.xml @@ -0,0 +1,213 @@ + + + 206 + 00\01 + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.157171626006094822772795196788 + 1.2.840.10008.1.2.1 + 1.3.6.1.4.1.22213.1.143 + 0.5 + POSDA + ISO_IR 100 + ORIGINAL\PRIMARY\AXIAL\CT_SOM5 SPI + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.157171626006094822772795196788 + 20040912 + 20040912 + 20040912 + 20040912 + 20040912062713.470000 + 062339.242000 + 062946.132000 + 062713.470000 + 062713.470000 + + CT + SIEMENS + + chest_abdomen_pelvis__w + + + IMG2002 + FHS + CTA ANGIOGRAM CHEST/ABD/PELVIS W PROCESSING + + + arterial + SOMATOM Definition Edge + + + 1.2.840.10008.3.1.2.3.1 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.179475577869989445825322252923 + + + + + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.297042649168128363219752116592 + + + + + 1.3.12.2.1107.5.9.1 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.688401930884469382984667769886 + + + 1.3.6.1.4.1.14519.5.2.1.6919.4624.688401930884469382984667769886 + SIEMENS CT VA1 DUMMY + KiTS-00136 + KiTS-00136 + + M + 052Y + 4 + -49 + Days offset from surgery + -49.0 + SURGERY + YES + Per DICOM PS 3.15 AnnexE. Details in 0012,0064 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113104 + DCM + Clean Structured Content Option + + + 113105 + DCM + Clean Descriptors Option + + + 113107 + DCM + Retain Longitudinal Temporal Information Modified Dates Option + + + 113108 + DCM + Retain Patient Characteristics Option + + + 113109 + DCM + Retain Device Identity Option + + + 113111 + DCM + Retain Safe Private Option + + + CTP + C4KC-KiTS + 69194624 + None + ABDOMEN + 5 + 100 + 500 + 0 + syngo CT VA48A + PE_CAP + 0 + 062615.760996 + 062615.760996 + 0 + 0 + 0 + 0 + 350 + 1085.6 + 595 + 0 + 151 + CW + 500 + 264 + 220 + FLAT + 30 + 1.2 + 20040912 + + I40f\2 + FFS + 0.6 + 38.4 + 46.0 + 23.0 + 0.6 + 0.0\-151.0\-504.3 + -5.0\-151.0\-504.3 + XYZ_EC + 53.0769 + 8.67425947826087 + + + 113691 + DCM + IEC Body Dosimetry Phantom + + + 0.679\0.714\0.746 + SIEMENS CT VA0 COAD + 0 + 0.9236 + -1.6565 + 0 + 23 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.179475577869989445825322252923 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.927472659782721902556191020020 + + 8 + 7 + 51 + -179.658203125\-325.658203125\-504.3 + 1\0\0\0\1\0 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.240499823560899143234666603682 + + 504.3 + + SIEMENS MED + 5\0 + 1 + MONOCHROME2 + 512 + 512 + 0.68359375\0.68359375 + 16 + 12 + 11 + 0 + 0 + 2419 + MODIFIED + 60\-600 + 375\1200 + -1024 + 1 + HU + WINDOW1\WINDOW2 + SIEMENS CSA HEADER + SIEMENS MEDCOM HEADER + CTA ANGIOGRAM CHEST/ABD/PELVIS W PROCESSING + + + IMG2002 + FHS + CTA ANGIOGRAM CHEST/ABD/PELVIS W PROCESSING + + + STENTOR + diff --git a/server/tests/dicom/ct-chest-lidc-idri-instance.dcm b/server/tests/dicom/ct-chest-lidc-idri-instance.dcm new file mode 100644 index 0000000000..84ad9adfb6 Binary files /dev/null and b/server/tests/dicom/ct-chest-lidc-idri-instance.dcm differ diff --git a/server/tests/dicom/ct-chest-lidc-idri-instance.xml b/server/tests/dicom/ct-chest-lidc-idri-instance.xml new file mode 100644 index 0000000000..59f5e74dfb --- /dev/null +++ b/server/tests/dicom/ct-chest-lidc-idri-instance.xml @@ -0,0 +1,115 @@ + + + 204 + 00\01 + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.147549101770122056857403430614 + 1.2.840.10008.1.2 + 1.3.6.1.4.1.22213.1.143 + 0.5 + POSDA + ISO_IR 100 + ORIGINAL\PRIMARY\AXIAL + + + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.147549101770122056857403430614 + 20000101 + 20000101 + 20000101 + 20000101 + 20000101 + 20000101 + 20000101 + + + + + 0 + IMA SCAN + + CT + GE MEDICAL SYSTEMS + + LightSpeed Ultra + + LIDC-IDRI-0580 + + + + 20000101 + YES + DCM:113100/113105/113107/113108/113109/113111 + CTP + LIDC-IDRI + 62796001 + CHEST + RM + HELICAL MODE + 1.250000 + 120 + 0.500000 + 500.000000 + LightSpeedApps304.3_H3.1M3 + 360.000000 + 949.075012 + 541.000000 + 0.000000 + 156.500000 + 504 + 80 + 960 + BODY FILTER + 10 + 0.700000 + STANDARD + 5.787642 + FFS + 1.3.6.1.4.1.14519.5.2.1.6279.6001.173480979711457247360986415860 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.237215747217294006286437405216 + + 30811 + 1 + 126 + 180\-180\0 + -184.600006\-174.800003\-127.230003 + -1\0\0\0\1\0 + 1.000000\0.000000\0.000000\0.000000\1.000000\0.000000 + -127.23 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.269849279208588310379687197292 + PLANAR + SN + -127.230003 + 1 + MONOCHROME2 + 2 + 512 + 512 + 007.031250e-01\007.031250e-01 + RECT + NONE + 16 + 16 + 15 + 1 + 63536 + 32736 + MODIFIED + 50 + 500 + -1024 + 1 + + 20000101 + 20000101 + 20000101 + 20000101 + + + + + + Removed by CTP + Removed by CTP + + diff --git a/server/tests/dicom/ct-lung-screening-nlst-instance.dcm b/server/tests/dicom/ct-lung-screening-nlst-instance.dcm new file mode 100644 index 0000000000..e5e2d1d1ab Binary files /dev/null and b/server/tests/dicom/ct-lung-screening-nlst-instance.dcm differ diff --git a/server/tests/dicom/ct-lung-screening-nlst-instance.xml b/server/tests/dicom/ct-lung-screening-nlst-instance.xml new file mode 100644 index 0000000000..302c24f405 --- /dev/null +++ b/server/tests/dicom/ct-lung-screening-nlst-instance.xml @@ -0,0 +1,118 @@ + + + 190 + 00\01 + 1.2.840.10008.5.1.4.1.1.2 + 1.2.840.113654.2.55.12882863977514288242837811272669865908 + 1.2.840.10008.1.2.1 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.31 + ISO_IR 100 + ORIGINAL\PRIMARY\AXIAL\CT_SOM5 SPI + 1.2.840.10008.5.1.4.1.1.2 + 1.2.840.113654.2.55.12882863977514288242837811272669865908 + 19990102 + + 888953 + CT + SIEMENS + + S13 + NLST-LSS + 0,OPA,SE,VZOOM,B30f,278,2,120,60,30,na + Volume Zoom + 061226^LSS + 112516 + + + 061226 + National Cancer Institute + NCT00047385 + NLST-LSS + + + 061226 + T0 + Washington U (images), Westat (assoc. data) + YES + 113100\113101\113103\113105\113109 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113103 + DCM + Clean Graphics Option + + + 113105 + DCM + Clean Descriptors Option + + + 113109 + DCM + Retain Device Identity Option + + + CTP + NLST + NLST + LSS\00 + 70049004 + CHEST + 2 + 120 + VA40C + 278 + 1040 + 570 + 0 + 145 + CW + 500 + 120 + 30 + 0 + 14 + 1.2 + B30f + 1.2.840.113654.2.55.33575893932308185246496913106863435791 + 1.2.840.113654.2.55.57669092321627294898588883865157458147 + + 6 + 3 + 107 + + -138.72852\-260.72852\-337.4 + 1\0\0\0\1\0 + 1.2.840.113654.2.55.85903792958366369381906191157480138431 + + 337.4 + 1 + MONOCHROME2 + 2 + 512 + 512 + 005.429688e-01\005.429688e-01 + RECT + NONE + 16 + 12 + 11 + 0 + 32736 + 35\400 + 400\3000 + -1024 + 1 + WINDOW1\WINDOW2 + diff --git a/server/tests/dicom/mg-mammography-cbis-ddsm-instance.dcm b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.dcm new file mode 100644 index 0000000000..dfd7bb1a52 Binary files /dev/null and b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.dcm differ diff --git a/server/tests/dicom/mg-mammography-cbis-ddsm-instance.xml b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.xml new file mode 100644 index 0000000000..c87ece87e7 --- /dev/null +++ b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.xml @@ -0,0 +1,49 @@ + + + 194 + 00\01 + 1.2.840.10008.5.1.4.1.1.7 + 1.3.6.1.4.1.9590.100.1.2.145141605511468979636497478400886398073 + 1.2.840.10008.1.2 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.35 + ISO_IR 100 + 1.2.840.10008.5.1.4.1.1.7 + 1.3.6.1.4.1.9590.100.1.2.145141605511468979636497478400886398073 + 20161004 + 20160426 + 152851 + 135218.377000 + + MG + WSD + + cropped images + Mass-Test_P_01510_RIGHT_CC_1 + Mass-Test_P_01510_RIGHT_CC_1 + + + CTP + CBIS-DDSM + 43372602 + BREAST + MathWorks + MATLAB + 1.3.6.1.4.1.9590.100.1.2.267826310912478261209862427462784808028 + 1.3.6.1.4.1.9590.100.1.2.88158044112350000736236683583018260323 + DDSM + 1 + 1 + CC + R + 1 + MONOCHROME2 + 386 + 386 + 16 + 16 + 15 + 0 + 38064 + 65535 + diff --git a/server/tests/dicom/mr-prostate-prostatex-instance.dcm b/server/tests/dicom/mr-prostate-prostatex-instance.dcm new file mode 100644 index 0000000000..9d27edbdb5 Binary files /dev/null and b/server/tests/dicom/mr-prostate-prostatex-instance.dcm differ diff --git a/server/tests/dicom/mr-prostate-prostatex-instance.xml b/server/tests/dicom/mr-prostate-prostatex-instance.xml new file mode 100644 index 0000000000..a89f5ad7d4 --- /dev/null +++ b/server/tests/dicom/mr-prostate-prostatex-instance.xml @@ -0,0 +1,181 @@ + + + 196 + 00\01 + 1.2.840.10008.5.1.4.1.1.4 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.174080670823095725391876950759 + 1.2.840.10008.1.2.1 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.35 + ISO_IR 100 + DERIVED\PRIMARY\DIFFUSION\TRACEW\DIS2D + 20120222 + 093124.703000 + 1.2.840.10008.5.1.4.1.1.4 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.174080670823095725391876950759 + 20120222 + 20120222 + 20120222 + 20120222 + 091218.078000 + 093124.609000 + 093113.347500 + 093124.703000 + 8913853518574410 + MR + SIEMENS + MR prostaat kanker detectie_mc MCAPRODET + ep2d_diff_tra_DYNDIST + Skyra + + + 1.2.840.10008.3.1.2.3.1 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.989962386484007264256688307298 + + + ProstateX-0204 + ProstateX-0204 + + M + 068Y + 1.85 + 78 + YES + DIAG-ANON 0.3\Per DICOM PS 3.15 AnnexE. Details in 0012,0064 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113104 + DCM + Clean Structured Content Option + + + 113105 + DCM + Clean Descriptors Option + + + 113107 + DCM + Retain Longitudinal Temporal Information Modified Dates Option + + + 113108 + DCM + Retain Patient Characteristics Option + + + 113109 + DCM + Retain Device Identity Option + + + 113111 + DCM + Retain Safe Private Option + + + CTP + PROSTATEx + 73105101 + PROSTATE + EP + SK\SP\OSP + PFP\FS + 2D + *ep_b400t + N + 3 + 2700 + 63 + 8 + 123.240954 + 1H + 1 + 3 + 3 + 95 + 48 + 100 + 65.625 + 1500 + syngo MR D11 + ep2d_diff_tra + Body + 0\128\84\0 + ROW + 90 + N + 0.57373217280629 + 0 + FFS + SIEMENS MR HEADER + 33\35 + 34\30\30\20 + 49\53\4F\54\52\4F\50\49\43\20 + 46\61\73\74 + 4E\6F + 00\00\00\00\00\00\00\00\6E\F8\FF\FF + 00\00\00\00\00\00\00\00\73\F8\FF\FF + 30\5C\30\5C\2D\35 + DC\2C\1C\10\7A\C9\52\C0\22\8E\33\55\09\EE\59\C0\75\95\8B\84\78\4C\49\40 + 31\39\33\2E\36\37\35\20 + 31\20 + 32\36\30\30 + 77\BE\9F\1A\2F\9D\34\40 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.989962386484007264256688307298 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.184815022142880568898393767017 + + 6 + 1 + 29 + -75.148075130013\-103.71931963112\45.597427910633 + 1\-1.885178e-010\8.07988e-011\2.051034e-010\0.91913534192834\-0.3939419033541 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.221351907157888248216888359615 + + 1.0508213036143 + 1 + MONOCHROME2 + 128 + 84 + 2\2 + 16 + 12 + 11 + 0 + 0 + 320 + MODIFIED + 89 + 239 + Algo1 + SIEMENS CSA HEADER + SIEMENS MEDCOM HEADER2 + 63\6F\6D\20 + MITRA LINKED ATTRIBUTES 1.0 + AGFA PACS Archive Mirroring 1.0 + ARRIVED + MED + MR prostaat kanker detectie_mc MCAPRODET + MR prostaat kanker detectie_mc + MITRA OBJECT UTF8 ATTRIBUTES 1.0 + 20120222 + 091218.156000 + MR prostaat kanker detectie_mc + + SIEMENS MR HEADER + 49\4D\41\47\45\20\4E\55\4D\20\34\20 + 31\2E\30\20 + 38\34\70\2A\31\32\38\73 + 42\4F\31\2C\32\3B\53\50\35\2C\36\20 + 2B\4C\50\48 + diff --git a/server/tests/dicom/pt-breast-qin-01-instance.dcm b/server/tests/dicom/pt-breast-qin-01-instance.dcm new file mode 100644 index 0000000000..567b5ada7e Binary files /dev/null and b/server/tests/dicom/pt-breast-qin-01-instance.dcm differ diff --git a/server/tests/dicom/pt-breast-qin-01-instance.xml b/server/tests/dicom/pt-breast-qin-01-instance.xml new file mode 100644 index 0000000000..3272ec9351 --- /dev/null +++ b/server/tests/dicom/pt-breast-qin-01-instance.xml @@ -0,0 +1,169 @@ + + + 198 + 00\01 + 1.2.840.10008.5.1.4.1.1.128 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.246092060369879550027417906384 + 1.2.840.10008.1.2.1 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.31 + ISO_IR 100 + ORIGINAL\PRIMARY + 19930102 + 190248 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.335605164322770524664245774978 + 1.2.840.10008.5.1.4.1.1.128 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.246092060369879550027417906384 + 19930102 + 19930102 + 19930102 + 19930102 + 185346.00 + 185833.00 + 185833.00 + 190248 + 3195263453823563 + PT + GE MEDICAL SYSTEMS + + vumcct + BREAST PRONE + PET AC 3DWB + Discovery STE + QIN-BREAST-01-0020 + QIN-BREAST-01-0020 + + F + 045Y + 1.6 + 65 + + + YES + DCM:113100/113105/113107/113108/113109/113111 + CTP + QIN-BREAST + 81627003 + BREAST + 3.27 + TIME + MANU + 0 + 0 + 41.04 + PRONE BREAST + 0 + 0 + 0 + 0 + 700 + 0 + CYLINDRICAL RING + 700\153 + NONE + 120000 + HFP + 1.3.6.1.4.1.14519.5.2.1.8162.7003.301759690586071225280046587327 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.222925049784905277500431157002 + + 3 + 56 + 347.265625\347.265625\-263.80999755859 + -1\0\0\0\-1\0 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.146772878946225576316847306304 + V + -263.81 + 1 + MONOCHROME2 + 128 + 128 + 5.46875\5.46875 + DECY\ATTN\SCAT\DTIM\RAN\DCAL\SLSENS\NORM + 16 + 16 + 15 + 1 + 0 + 32767 + MODIFIED + 0 + 0.413549 + 00 + + + 425 + 650 + + + + + FDG -- fluorodeoxyglucose + 0 + 175700.00 + 175800.00 + 429823712 + 6588 + 0.97000002861023 + 19930102175700.00 + 19930102175800.00 + + + C-111A1 + SNM3 + 18F + + + + + C-B1031 + SNM3 + FDG -- fluorodeoxyglucose + + + + + 83 + NONE + + + F-10450 + SNM3 + recumbent + + + + + F-10310 + SNM3 + prone + + + + + F-10470 + SNM3 + headfirst + + + STATIC\IMAGE + BQML + EMISSION + SING + measured,, 0.096000 cm-1, + START + 3D IR + 0 + Model Based + 1\2 + 2 + 0 + 0 + 0 + 0 + 1 + 1.00633 + 1523 + 0.366749 + 1.30417 + 28 + diff --git a/server/tests/dicom/us-lymph-node-cmb-lca-instance.dcm b/server/tests/dicom/us-lymph-node-cmb-lca-instance.dcm new file mode 100644 index 0000000000..f155d50239 Binary files /dev/null and b/server/tests/dicom/us-lymph-node-cmb-lca-instance.dcm differ diff --git a/server/tests/dicom/us-lymph-node-cmb-lca-instance.xml b/server/tests/dicom/us-lymph-node-cmb-lca-instance.xml new file mode 100644 index 0000000000..186782c02d --- /dev/null +++ b/server/tests/dicom/us-lymph-node-cmb-lca-instance.xml @@ -0,0 +1,148 @@ + + + 208 + 00\01 + 1.2.840.10008.5.1.4.1.1.6.1 + 1.3.6.1.4.1.14519.5.2.1.185577958846157191089023023733319573972 + 1.2.840.10008.1.2.1 + 1.3.6.1.4.1.22213.1.143 + 0.5 + POSDA + ISO_IR 192 + DERIVED\PRIMARY\SMALL PARTS + 19591020 + 113433 + 1.2.840.10008.5.1.4.1.1.6.1 + 1.3.6.1.4.1.14519.5.2.1.185577958846157191089023023733319573972 + 19591020 + 19591020 + 19591020 + 19591020113433 + 100000 + 110358 + 113433 + + US + FOR PRESENTATION + Philips Medical Systems + + US_Biopsy_RCervicalNode + + + IMG1101 + GEIIS + 0 + US GUIDED THYROID FNA + + + US GUIDED LYMPH NODE SUPERFICIAL BIOPSY + EPIQ 5G + + + 1.2.840.100008.3.1.2.3.1 + 1.3.6.1.4.1.14519.5.2.1.207522269430030124341950649812029134159 + + + + + 1.2.840.10008.3.1.2.3.3 + 1.3.6.1.4.1.14519.5.2.1.241721883221006165848038945169821191225 + + + + MSB-02120 + MSB-02120 + + O + + 4 + -72.0 + REGISTRATION + YES + Per DICOM PS 3.15 AnnexE. Details in 0012,0064 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113104 + DCM + Clean Structured Content Option + + + 113105 + DCM + Clean Descriptors Option + + + 113107 + DCM + Retain Longitudinal Temporal Information Modified Dates Option + + + 113108 + DCM + Retain Patient Characteristics Option + + + 113109 + DCM + Retain Device Identity Option + + + 113111 + DCM + Retain Safe Private Option + + + CTP + CMB-LCA + 16001202 + EPIQ 5G_7.0.5.962 + 0 + L15_7io\54108 + SM_PRTS_SUPERFICIAL + + + 1 + 1 + 2 + 14 + 38 + 1010 + 758 + 3 + 3 + 0.003708797278190173 + 0.003708797278190173 + + + 1.3.6.1.4.1.14519.5.2.1.207522269430030124341950649812029134159 + 1.3.6.1.4.1.14519.5.2.1.58866608043609699358076981026977397142 + + 1 + 10 + 1 + MONOCHROME2 + 768 + 1024 + 0 + 8 + 8 + 7 + 0 + NO + MODIFIED + 127 + 254 + 00 + + IDENTITY + diff --git a/server/tests/test-1597-input-missing-elements.xml b/server/tests/test-1597-input-missing-elements.xml new file mode 100644 index 0000000000..b5789444eb --- /dev/null +++ b/server/tests/test-1597-input-missing-elements.xml @@ -0,0 +1,261 @@ + + + + 00 + + + + + + 00 + + + + + + ZZ + + + SUBMITTERID + + + ZZ + + + PAYERID + + + 260330 + + + 1430 + + + ^ + + + 00501 + + + 000000001 + + + 0 + + + T + + + + + + + + + HS + + + SUBMITTERID + + + PAYERID + + + 20260330 + + + 1430 + + + 1 + + + X + + + 005010X279A1 + + + + + 270 + + + 0001 + + + 000010001 + + + + + 0022 + + + 13 + + + REF123456789 + + + 20260330 + + + 1430 + + + + + 1 + + + + 20 + + + 1 + + + + + PR + + + 2 + + + GREAT HEALTH PLAN + + + PI + + + PAYER12345 + + + + + 2 + + + 1 + + + 21 + + + 1 + + + + + 1P + + + 2 + + + COMMUNITY HOSPITAL + + + + + + + XX + + + 1234567890 + + + + + 3 + + + 2 + + + 22 + + + 0 + + + + + 1 + + + 987654321 + + + 1234567890 + + + + + IL + + + 1 + + + SMITH + + + JOHN + + + M + + + + + MI + + + SUB123456789 + + + + + D8 + + + 19800101 + + + M + + + + + 30 + + + + + 13 + + + 0001 + + + + + 1 + + + 1 + + + + + 1 + + + 000000001 + + + diff --git a/server/tests/test-edi-input.txt b/server/tests/test-edi-input.txt new file mode 100644 index 0000000000..e1a30e694e --- /dev/null +++ b/server/tests/test-edi-input.txt @@ -0,0 +1,16 @@ +ISA*00* *00* *ZZ*SUBMITTERID *ZZ*PAYERID *260330*1430*^*00501*000000001*0*T*:~ +GS*HS*SUBMITTERID*PAYERID*20260330*1430*1*X*005010X279A1~ +ST*270*0001*000010001~ +BHT*0022*13*REF123456789*20260330*1430~ +HL*1**20*1~ +NM1*PR*2*GREAT HEALTH PLAN*****PI*PAYER12345~ +HL*2*1*21*1~ +NM1*1P*2*COMMUNITY HOSPITAL*****XX*1234567890~ +HL*3*2*22*0~ +TRN*1*987654321*1234567890~ +NM1*IL*1*SMITH*JOHN*M***MI*SUB123456789~ +DMG*D8*19800101*M~ +EQ*30~ +SE*13*0001~ +GE*1*1~ +IEA*1*000000001~ diff --git a/server/tests/test-edi-output.xml b/server/tests/test-edi-output.xml new file mode 100644 index 0000000000..0a7ded8aeb --- /dev/null +++ b/server/tests/test-edi-output.xml @@ -0,0 +1,265 @@ + + + + 00 + + + + + + 00 + + + + + + ZZ + + + SUBMITTERID + + + ZZ + + + PAYERID + + + 260330 + + + 1430 + + + ^ + + + 00501 + + + 000000001 + + + 0 + + + T + + + + + + + + + HS + + + SUBMITTERID + + + PAYERID + + + 20260330 + + + 1430 + + + 1 + + + X + + + 005010X279A1 + + + + + 270 + + + 0001 + + + 000010001 + + + + + 0022 + + + 13 + + + REF123456789 + + + 20260330 + + + 1430 + + + + + 1 + + + + 20 + + + 1 + + + + + PR + + + 2 + + + GREAT HEALTH PLAN + + + + + + + PI + + + PAYER12345 + + + + + 2 + + + 1 + + + 21 + + + 1 + + + + + 1P + + + 2 + + + COMMUNITY HOSPITAL + + + + + + + XX + + + 1234567890 + + + + + 3 + + + 2 + + + 22 + + + 0 + + + + + 1 + + + 987654321 + + + 1234567890 + + + + + IL + + + 1 + + + SMITH + + + JOHN + + + M + + + + + MI + + + SUB123456789 + + + + + D8 + + + 19800101 + + + M + + + + + 30 + + + + + 13 + + + 0001 + + + + + 1 + + + 1 + + + + + 1 + + + 000000001 + + + diff --git a/server/tests/test-ncpdp-51-request-input.txt b/server/tests/test-ncpdp-51-request-input.txt new file mode 100644 index 0000000000..7657c0435c --- /dev/null +++ b/server/tests/test-ncpdp-51-request-input.txt @@ -0,0 +1 @@ +00000051B1ASDF 1011234567890 20260913 AM01CY123456789C701AM04C2123456789AM07EM1D298765432E103D711111022222E728000D30D57D61D80DF0DJ3DE20260913AM03EZ01DB1234567890AM11D9136EDC80{DX00{DQ216EDU216EDN01 \ No newline at end of file diff --git a/server/tests/test-ncpdp-51-request-output.xml b/server/tests/test-ncpdp-51-request-output.xml new file mode 100644 index 0000000000..4e9b41e4eb --- /dev/null +++ b/server/tests/test-ncpdp-51-request-output.xml @@ -0,0 +1,50 @@ + + + 000000 + 51 + B1 + ASDF + 1 + 01 + 1234567890 + 20260913 + + + + 123456789 + 01 + + + 123456789 + + + + + 1 + 98765432 + 03 + 11111022222 + 28000 + 0 + 7 + 1 + 0 + 0 + 3 + 20260913 + + + 01 + 1234567890 + + + 136E + 80{ + 00{ + 216E + 216E + 01 + + + + diff --git a/server/tests/test-ncpdp-51-response-input.txt b/server/tests/test-ncpdp-51-response-input.txt new file mode 100644 index 0000000000..57f3661576 --- /dev/null +++ b/server/tests/test-ncpdp-51-response-input.txt @@ -0,0 +1 @@ +51B11A011234567890 20260913AM25C2123456789AM29AM21ANPF354716cb1bb9646a9bc8cAM22EM1D298765432AM23F500{F600{F7216EF9216EFM6 \ No newline at end of file diff --git a/server/tests/test-ncpdp-51-response-output.xml b/server/tests/test-ncpdp-51-response-output.xml new file mode 100644 index 0000000000..b312ec0964 --- /dev/null +++ b/server/tests/test-ncpdp-51-response-output.xml @@ -0,0 +1,34 @@ + + + 51 + B1 + 1 + A + 01 + 1234567890 + 20260913 + + + 123456789 + + + + + + P + 54716cb1bb9646a9bc8c + + + 1 + 98765432 + + + 00{ + 00{ + 216E + 216E + 6 + + + + diff --git a/server/tests/test-ncpdp-d0-request-input.txt b/server/tests/test-ncpdp-d0-request-input.txt new file mode 100644 index 0000000000..776ecb8a98 --- /dev/null +++ b/server/tests/test-ncpdp-d0-request-input.txt @@ -0,0 +1 @@ +000000D0B1ASDF 1011234567890 20260913 AM01CY123456789C701AM04C2123456789AM07EM1D298765432E103D711111022222E728000D30D57D61D80DF0DJ3DE20260913U75AM03EZ01DB1234567890AM11D9136EDC80{DX00{DQ216EDU216EDN01AM158CEX3QExample3U123 Anywhere St.5JNowhere3VIN6D12345 \ No newline at end of file diff --git a/server/tests/test-ncpdp-d0-request-output.xml b/server/tests/test-ncpdp-d0-request-output.xml new file mode 100644 index 0000000000..0346830984 --- /dev/null +++ b/server/tests/test-ncpdp-d0-request-output.xml @@ -0,0 +1,59 @@ + + + 000000 + D0 + B1 + ASDF + 1 + 01 + 1234567890 + 20260913 + + + + 123456789 + 01 + + + 123456789 + + + + + 1 + 98765432 + 03 + 11111022222 + 28000 + 0 + 7 + 1 + 0 + 0 + 3 + 20260913 + 5 + + + 01 + 1234567890 + + + 136E + 80{ + 00{ + 216E + 216E + 01 + + + EX + Example + 123 Anywhere St. + Nowhere + IN + 12345 + + + + diff --git a/server/tests/test-ncpdp-d0-response-input.txt b/server/tests/test-ncpdp-d0-response-input.txt new file mode 100644 index 0000000000..d2960b217b --- /dev/null +++ b/server/tests/test-ncpdp-d0-response-input.txt @@ -0,0 +1 @@ +D0B11A011234567890 20260913AM25C2123456789AM29AM21ANPF354716cb1bb9646a9bc8cAM22EM1D298765432AM23F500{F600{F7216EF9216EFM6 \ No newline at end of file diff --git a/server/tests/test-ncpdp-d0-response-output.xml b/server/tests/test-ncpdp-d0-response-output.xml new file mode 100644 index 0000000000..addf81dcee --- /dev/null +++ b/server/tests/test-ncpdp-d0-response-output.xml @@ -0,0 +1,34 @@ + + + D0 + B1 + 1 + A + 01 + 1234567890 + 20260913 + + + 123456789 + + + + + + P + 54716cb1bb9646a9bc8c + + + 1 + 98765432 + + + 00{ + 00{ + 216E + 216E + 6 + + + +