getPortsInUse() {
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/OracleJdbcDaoTest.java b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/OracleJdbcDaoTest.java
new file mode 100644
index 0000000000..fd9cc28934
--- /dev/null
+++ b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/OracleJdbcDaoTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.donkey.server.data.jdbc;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import com.mirth.connect.donkey.model.message.ContentType;
+import com.mirth.connect.donkey.model.message.MessageContent;
+import com.mirth.connect.donkey.server.Donkey;
+import com.mirth.connect.donkey.server.channel.Statistics;
+import com.mirth.connect.donkey.server.data.DonkeyDaoException;
+import com.mirth.connect.donkey.server.data.StatisticsUpdater;
+import com.mirth.connect.donkey.util.SerializerProvider;
+
+/**
+ * Oracle is the only DAO that actually closes statements, so it is the only one where closing
+ * the wrong statement at the wrong time is observable.
+ */
+public class OracleJdbcDaoTest {
+
+ private static final String CHANNEL_ID = "abc";
+
+ private OracleJdbcDao dao;
+ private PreparedStatement statement;
+
+ @Before
+ public void before() throws SQLException {
+ Donkey donkey = mock(Donkey.class);
+ Connection connection = mock(Connection.class);
+ QuerySource querySource = mock(QuerySource.class);
+ PreparedStatementSource statementSource = mock(PreparedStatementSource.class);
+ SerializerProvider serializerProvider = mock(SerializerProvider.class);
+ StatisticsUpdater statisticsUpdater = mock(StatisticsUpdater.class);
+ Statistics currentStats = mock(Statistics.class);
+ Statistics totalStats = mock(Statistics.class);
+
+ dao = spy(new OracleJdbcDao(donkey, connection, querySource, statementSource, serializerProvider, false, false, false, false, statisticsUpdater, currentStats, totalStats, ""));
+
+ statement = mock(PreparedStatement.class);
+ doReturn(statement).when(dao).prepareStatement(eq("batchInsertMessageContent"), eq(CHANNEL_ID));
+ }
+
+ /**
+ * The batch lives on the cached statement, so the statement has to survive every
+ * batchInsertMessageContent() call and only be closed once the batch has been executed.
+ * Closing it earlier silently discarded the source content on Oracle.
+ */
+ @Test
+ public void testBatchInsertMessageContentKeepsStatementOpenUntilExecuted() throws SQLException {
+ dao.batchInsertMessageContent(content(ContentType.PROCESSED_RAW, "processed raw"));
+ dao.batchInsertMessageContent(content(ContentType.TRANSFORMED, "transformed"));
+ dao.batchInsertMessageContent(content(ContentType.ENCODED, "encoded"));
+
+ verify(statement, times(3)).addBatch();
+ verify(statement, never()).close();
+
+ dao.executeBatchInsertMessageContent(CHANNEL_ID);
+
+ InOrder inOrder = inOrder(statement);
+ inOrder.verify(statement, times(3)).addBatch();
+ inOrder.verify(statement).executeBatch();
+ inOrder.verify(statement).clearBatch();
+ inOrder.verify(statement).close();
+ }
+
+ /** A failed batch must not be left behind for the next message to execute. */
+ @Test
+ public void testFailedBatchInsertClearsTheBatch() throws SQLException {
+ doThrow(new SQLException("no")).when(statement).addBatch();
+
+ try {
+ dao.batchInsertMessageContent(content(ContentType.ENCODED, "encoded"));
+ fail("Expected a DonkeyDaoException");
+ } catch (DonkeyDaoException e) {
+ // expected
+ }
+
+ verify(statement).clearBatch();
+ }
+
+ private static MessageContent content(ContentType contentType, String content) {
+ return new MessageContent(CHANNEL_ID, 1L, 0, contentType, content, "RAW", false);
+ }
+}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/server/queue/ConnectorMessageQueueTest.java b/donkey/src/test/java/com/mirth/connect/donkey/server/queue/ConnectorMessageQueueTest.java
new file mode 100644
index 0000000000..4cc8b63c14
--- /dev/null
+++ b/donkey/src/test/java/com/mirth/connect/donkey/server/queue/ConnectorMessageQueueTest.java
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package com.mirth.connect.donkey.server.queue;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.mirth.connect.donkey.model.message.ConnectorMessage;
+import com.mirth.connect.donkey.model.message.Status;
+import com.mirth.connect.donkey.server.Donkey;
+import com.mirth.connect.donkey.server.event.EventDispatcher;
+
+/**
+ * The buffer arithmetic in {@link ConnectorMessageQueue}: how much of the queue is held in
+ * memory, what {@code add} does once the buffer is full, and that polling still walks the
+ * whole queue in order by going back to the data source.
+ *
+ * The queue is a window onto rows the engine has already committed, so the data source
+ * here is a plain map standing in for those rows rather than a database. That is the whole
+ * point of testing it at this level: the arithmetic is the same on every dialect, and a fake
+ * makes the buffer boundaries exact instead of a race against a live channel.
+ */
+public class ConnectorMessageQueueTest {
+
+ private static final String CHANNEL_ID = "ConnectorMessageQueueTest";
+ private static final String CHANNEL_NAME = "ConnectorMessageQueueTest";
+ private static final String SERVER_ID = "ConnectorMessageQueueTestServer";
+ private static final int META_DATA_ID = 0;
+
+ private static final int BUFFER_CAPACITY = 3;
+ private static final int QUEUE_SIZE = 10;
+
+ private FakeDataSource dataSource;
+ private SourceQueue queue;
+
+ /**
+ * {@link ConnectorMessageQueue} dispatches a queue-size event on every mutation and reads
+ * its dispatcher off the {@link Donkey} singleton, which is only populated by a started
+ * engine. Inject a mock one instead, as ChannelTest does.
+ */
+ @BeforeClass
+ public static void injectDonkey() {
+ Donkey donkey = mock(Donkey.class);
+ when(donkey.getEventDispatcher()).thenReturn(mock(EventDispatcher.class));
+
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ requestStaticInjection(Donkey.class);
+ bind(Donkey.class).toInstance(donkey);
+ }
+ });
+ injector.getInstance(Donkey.class);
+
+ assertTrue("Donkey singleton injection failed", donkey == Donkey.getInstance());
+ }
+
+ @Before
+ public void setUp() {
+ dataSource = new FakeDataSource();
+ queue = new SourceQueue();
+ queue.setBufferCapacity(BUFFER_CAPACITY);
+ queue.setDataSource(dataSource);
+ }
+
+ /**
+ * The buffer is a window, not a copy: filling it from a data source holding more messages
+ * than the buffer can take leaves the queue reporting the full size and the buffer the
+ * capacity.
+ */
+ @Test
+ public void fillBufferReadsAtMostTheBufferCapacity() {
+ storeMessages(QUEUE_SIZE);
+
+ queue.updateSize();
+ queue.fillBuffer();
+
+ assertEquals(BUFFER_CAPACITY, queue.getBufferCapacity());
+ assertEquals(QUEUE_SIZE, queue.size());
+ assertEquals(BUFFER_CAPACITY, queue.getBufferSize());
+ }
+
+ /**
+ * {@code add} is called once per message the engine has just committed. Up to the capacity
+ * it puts the message straight into the buffer; past it the buffer stops growing while the
+ * queue keeps counting, so the extra messages are only reachable through the data source.
+ */
+ @Test
+ public void addPastTheBufferCapacityGrowsTheQueueButNotTheBuffer() {
+ syncWithEmptyDataSource();
+
+ for (int i = 1; i <= BUFFER_CAPACITY; i++) {
+ queue.add(storeMessage(i));
+ assertEquals("queue size after adding message " + i, i, queue.size());
+ assertEquals("buffer size after adding message " + i, i, queue.getBufferSize());
+ }
+
+ for (int i = BUFFER_CAPACITY + 1; i <= QUEUE_SIZE; i++) {
+ queue.add(storeMessage(i));
+ assertEquals("queue size after adding message " + i, i, queue.size());
+ assertEquals("buffer size after adding message " + i, BUFFER_CAPACITY, queue.getBufferSize());
+ }
+ }
+
+ /**
+ * Polling past the buffer is the behaviour the capacity exists for: the queue refills from
+ * the data source when the buffer runs dry, so every message comes out exactly once and in
+ * the order the data source holds them, and the queue ends empty.
+ *
+ *
Every message here is committed and then added, which is what the engine guarantees by
+ * doing both under this queue's own lock.
+ * {@link #rowsCommittedWithoutBeingAddedAreStrandedUntilTheQueueResyncs} is the counterfactual
+ * for what that lock is holding off.
+ */
+ @Test
+ public void pollingRefillsTheBufferAndReturnsEveryMessageInOrder() {
+ syncWithEmptyDataSource();
+ for (int i = 1; i <= QUEUE_SIZE; i++) {
+ queue.add(storeMessage(i));
+ }
+
+ List polled = drain();
+
+ assertEquals(expectedIds(QUEUE_SIZE), polled);
+ assertEquals(0, queue.size());
+ assertTrue("the buffer only holds " + BUFFER_CAPACITY + " of " + QUEUE_SIZE + " messages, so"
+ + " draining the queue must have gone back to the data source for more",
+ dataSource.readCount > 1);
+ }
+
+ /**
+ * The queue's size is a count of what has been added to it, not of what is committed, so a
+ * row committed without a matching {@code add} is invisible to it: the queue hands over the
+ * message it does know about, then reports itself empty while those rows are still there.
+ *
+ * This is why the engine commits a source message and adds it to the queue under the same
+ * lock. Nothing a client can do opens this window - it takes a write that bypasses the queue,
+ * which is what this test does directly - but it is also what an unclean shutdown leaves
+ * behind, and the recovery at the end is the same resync a redeploy performs.
+ */
+ @Test
+ public void rowsCommittedWithoutBeingAddedAreStrandedUntilTheQueueResyncs() {
+ syncWithEmptyDataSource();
+
+ // Committed, but the add never happened.
+ int strandedCount = BUFFER_CAPACITY + 1;
+ storeMessages(strandedCount);
+
+ // A later message committed and added the ordinary way.
+ long addedId = strandedCount + 1;
+ queue.add(storeMessage(addedId));
+
+ assertEquals("the queue counts what was added to it", 1, queue.size());
+ assertEquals("every message is committed", strandedCount + 1, dataSource.getSize());
+
+ // The one message the queue knows about comes out, and then it calls itself empty.
+ ConnectorMessage polled = queue.poll();
+ assertEquals(addedId, polled.getMessageId());
+ dataSource.rows.remove(polled.getMessageId());
+ queue.finish(polled);
+ assertNull("the queue reports itself empty while committed rows are still there",
+ queue.poll());
+
+ // Resyncing against the data source is what finds them again, in order.
+ queue.invalidate(false, true);
+ queue.updateSize();
+
+ assertEquals(strandedCount, queue.size());
+ assertEquals(expectedIds(strandedCount), drain());
+ }
+
+ /**
+ * Shrinking the capacity discards the buffer rather than truncating it, because the
+ * messages it holds may no longer be the ones a smaller window should start from. The
+ * queue size is untouched and the next poll refills, so nothing is lost or reordered.
+ */
+ @Test
+ public void shrinkingTheBufferCapacityDiscardsTheBufferWithoutLosingMessages() {
+ storeMessages(QUEUE_SIZE);
+ queue.updateSize();
+ queue.fillBuffer();
+ assertEquals(BUFFER_CAPACITY, queue.getBufferSize());
+
+ queue.setBufferCapacity(1);
+
+ assertEquals(0, queue.getBufferSize());
+ assertEquals(QUEUE_SIZE, queue.size());
+ assertEquals(expectedIds(QUEUE_SIZE), drain());
+ }
+
+ /**
+ * Brings the queue in sync with an empty data source. {@code setDataSource} invalidates the
+ * queue, and until it is filled once, {@code add} takes its resync path rather than the
+ * buffering path under test.
+ */
+ private void syncWithEmptyDataSource() {
+ queue.updateSize();
+ queue.fillBuffer();
+ assertEquals(0, queue.size());
+ }
+
+ /**
+ * Polls the queue dry the way a queue thread does: every message that comes out is finished
+ * and its row removed, so a refill sees only what is genuinely still queued.
+ */
+ private List drain() {
+ List polled = new ArrayList();
+
+ ConnectorMessage connectorMessage;
+ while ((connectorMessage = queue.poll()) != null) {
+ polled.add(connectorMessage.getMessageId());
+ dataSource.rows.remove(connectorMessage.getMessageId());
+ queue.finish(connectorMessage);
+ }
+
+ assertNull(queue.poll());
+ return polled;
+ }
+
+ private void storeMessages(int count) {
+ for (int i = 1; i <= count; i++) {
+ storeMessage(i);
+ }
+ }
+
+ private ConnectorMessage storeMessage(long messageId) {
+ ConnectorMessage connectorMessage = new ConnectorMessage(CHANNEL_ID, CHANNEL_NAME, messageId, META_DATA_ID,
+ SERVER_ID, Calendar.getInstance(), Status.RECEIVED);
+ dataSource.rows.put(messageId, connectorMessage);
+ return connectorMessage;
+ }
+
+ private static List expectedIds(int count) {
+ List ids = new ArrayList();
+ for (long i = 1; i <= count; i++) {
+ ids.add(i);
+ }
+ return ids;
+ }
+
+ /**
+ * The committed queue rows, in message id order. The real data source runs a bounded
+ * {@code getConnectorMessages} query against the channel's queue table; this one pages the
+ * same way over a map, and counts the reads so a test can tell a refill happened.
+ */
+ private static final class FakeDataSource extends ConnectorMessageQueueDataSource {
+
+ final Map rows = new LinkedHashMap();
+ int readCount;
+
+ FakeDataSource() {
+ super(CHANNEL_ID, SERVER_ID, META_DATA_ID, Status.RECEIVED, false, null);
+ }
+
+ @Override
+ public int getSize() {
+ return rows.size();
+ }
+
+ @Override
+ public Map getItems(int offset, int limit) {
+ readCount++;
+
+ Map page = new LinkedHashMap();
+ for (ConnectorMessage connectorMessage : rows.values()) {
+ if (offset > 0) {
+ offset--;
+ } else if (page.size() < limit) {
+ page.put(connectorMessage.getMessageId(), connectorMessage);
+ } else {
+ break;
+ }
+ }
+ return page;
+ }
+ }
+}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java
index 4927f9b60c..b7df00e374 100644
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java
+++ b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java
@@ -11,14 +11,6 @@
import static org.junit.Assert.assertEquals;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.Types;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.AfterClass;
@@ -26,21 +18,21 @@
import org.junit.BeforeClass;
import org.junit.Test;
-import com.mirth.connect.donkey.model.message.Status;
import com.mirth.connect.donkey.server.Donkey;
import com.mirth.connect.donkey.server.StartException;
-import com.mirth.connect.donkey.server.channel.Channel;
import com.mirth.connect.donkey.server.controllers.ChannelController;
import com.mirth.connect.donkey.server.data.timed.TimedDaoFactory;
-import com.mirth.connect.donkey.test.util.TestSourceConnector;
import com.mirth.connect.donkey.test.util.TestUtils;
import com.mirth.connect.donkey.util.ActionTimer;
+/**
+ * What is left of these tests after the statistics ones moved to the CI harness: the statistics
+ * cases live in {@code smoketest/.../*StatisticsTest.java} (250-statistics), which reads the same
+ * counters over the client API against every dialect instead of against the database directly.
+ */
public class ChannelControllerTests {
private static int TEST_SIZE = 10;
private static String channelId = TestUtils.DEFAULT_CHANNEL_ID;
- private static String serverId = TestUtils.DEFAULT_SERVER_ID;
- private static String testMessage = TestUtils.TEST_HL7_MESSAGE;
private static ActionTimer daoTimer = new ActionTimer();
private Logger logger = LogManager.getLogger(this.getClass());
@@ -89,145 +81,4 @@ final public void testGetLocalChannelId() throws Exception {
ChannelController.getInstance().removeChannel(channelId);
}
}
-
- /*
- * Remove the channel corresponding to channelId (if applicable), assert: - The channel
- * statistics in the database are equal to the ones returned from getStatistics
- *
- * Create a new channel (use channelId), then assert: - The channel statistics in the database
- * are equal to the ones returned from getStatistics
- *
- * Send messages through the channel, and after each message, assert: - The channel statistics
- * in the database are equal to the ones returned from getStatistics
- */
- @Test
- public final void testGetTotals() throws Exception {
- Channel channel = null;
-
- try {
- logger.info("Testing ChannelController.getTotals...");
-
- ChannelController.getInstance().removeChannel(channelId);
- assertEquals(TestUtils.getChannelStatistics(channelId), ChannelController.getInstance().getStatistics().getChannelStats(channelId));
-
- channel = TestUtils.createDefaultChannel(channelId, serverId);
- channel.deploy();
- channel.start(null);
-
- assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channelId));
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
-
- assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channelId));
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- if (channel != null) {
- channel.stop();
- channel.undeploy();
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
- }
- }
-
- /*
- * Create a new default channel Insert some random initial source/destination/aggregate
- * statistics Start up the channel, send messages, and after each message assert: - The
- * statistics returned from getStatistics is equal to the difference between the statistics in
- * the database and the initial statistics
- */
- @Test
- public final void testGetStatistics() throws Exception {
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- try {
- logger.info("Testing ChannelController.getStatistics...");
-
- // Insert some initial statistics
- Connection connection = null;
- PreparedStatement statement = null;
- try {
- long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId);
- connection = TestUtils.getConnection();
- statement = connection.prepareStatement("DELETE FROM d_ms" + localChannelId);
- statement.executeUpdate();
- statement.close();
-
- for (Integer metaDataId : new Integer[] { null, 0, 1 }) {
- statement = connection.prepareStatement("INSERT INTO d_ms" + localChannelId + " (metadata_id, received, filtered, transformed, pending, sent, error) VALUES (?,?,?,?,?,?,?)");
- if (metaDataId != null) {
- statement.setInt(1, metaDataId);
- } else {
- statement.setNull(1, Types.INTEGER);
- }
- for (int i = 2; i <= 7; i++) {
- statement.setInt(i, (int) (Math.random() * 100));
- }
- statement.executeUpdate();
- statement.close();
- }
- connection.commit();
- } finally {
- TestUtils.close(statement);
- TestUtils.close(connection);
- }
-
- // Get the initial statistics
- Map> initialStats = TestUtils.getChannelStatistics(channel.getChannelId());
-
- channel.deploy();
- channel.start(null);
-
- // Send messages
- for (int i = 1; i <= TEST_SIZE; i++) {
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
-
- Map> dbStats = TestUtils.getChannelStatistics(channel.getChannelId());
- Map> vmStats = ChannelController.getInstance().getStatistics().getChannelStats(channel.getChannelId());
- Map> subtractedStats = subtractStats(dbStats, initialStats);
-
- // Assert that getStatistics returns the difference between the current database statistics and the initial statistics
- assertEquals(subtractedStats, vmStats);
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- channel.stop();
- channel.undeploy();
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
- }
-
- private Map> subtractStats(Map> minuend, Map> subtrahend) {
- Map> stats = new HashMap>();
-
- for (Integer metaDataId : joinSets(minuend.keySet(), subtrahend.keySet())) {
- Map connectorStats = new HashMap();
-
- for (Status status : Status.values()) {
- if (status != Status.QUEUED) {
- connectorStats.put(status, 0L);
- if (minuend.containsKey(metaDataId) && minuend.get(metaDataId).containsKey(status)) {
- connectorStats.put(status, minuend.get(metaDataId).get(status));
- }
- if (subtrahend.containsKey(metaDataId) && subtrahend.get(metaDataId).containsKey(status)) {
- connectorStats.put(status, connectorStats.get(status) - subtrahend.get(metaDataId).get(status));
- }
- }
- }
-
- stats.put(metaDataId, connectorStats);
- }
-
- return stats;
- }
-
- private Set joinSets(Set set1, Set set2) {
- Set joinedSet = new HashSet();
- joinedSet.addAll(set1);
- joinedSet.addAll(set2);
- return joinedSet;
- }
}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java
index 8a532f21ba..146e62e69d 100644
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java
+++ b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java
@@ -15,44 +15,23 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import java.math.BigDecimal;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.List;
-
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import com.mirth.connect.donkey.model.channel.DeployedState;
-import com.mirth.connect.donkey.model.channel.DestinationConnectorProperties;
-import com.mirth.connect.donkey.model.channel.DestinationConnectorPropertiesInterface;
-import com.mirth.connect.donkey.model.channel.MetaDataColumn;
-import com.mirth.connect.donkey.model.channel.MetaDataColumnException;
-import com.mirth.connect.donkey.model.channel.MetaDataColumnType;
-import com.mirth.connect.donkey.model.message.ConnectorMessage;
-import com.mirth.connect.donkey.model.message.ContentType;
import com.mirth.connect.donkey.model.message.Message;
-import com.mirth.connect.donkey.model.message.MessageContent;
-import com.mirth.connect.donkey.model.message.RawMessage;
import com.mirth.connect.donkey.server.Donkey;
import com.mirth.connect.donkey.server.StartException;
import com.mirth.connect.donkey.server.channel.Channel;
import com.mirth.connect.donkey.server.channel.ChannelException;
import com.mirth.connect.donkey.server.channel.DestinationChainProvider;
import com.mirth.connect.donkey.server.channel.DestinationConnector;
-import com.mirth.connect.donkey.server.channel.DispatchResult;
-import com.mirth.connect.donkey.server.channel.SourceConnector;
-import com.mirth.connect.donkey.server.channel.StorageSettings;
import com.mirth.connect.donkey.server.controllers.ChannelController;
import com.mirth.connect.donkey.test.util.TestChannel;
import com.mirth.connect.donkey.test.util.TestDestinationConnector;
-import com.mirth.connect.donkey.test.util.TestFilterTransformer;
-import com.mirth.connect.donkey.test.util.TestPostProcessor;
-import com.mirth.connect.donkey.test.util.TestPreProcessor;
import com.mirth.connect.donkey.test.util.TestSourceConnector;
import com.mirth.connect.donkey.test.util.TestUtils;
-import com.mirth.connect.donkey.test.util.TestUtils.MessageStorageMode;
public class ChannelTests {
final public static int TEST_SIZE = 50;
@@ -321,400 +300,20 @@ public final void testControllerRemoveChannel() throws Exception {
}
/*
- * Creates and deploys a channel, and asserts that: - No extra columns exist on the custom
- * metadata table
- *
- * Adds metadata columns (one of each type), redeploys the channel, and asserts that: - All the
- * added columns are in the database with the correct name and type
- *
- * Removes one of the columns, redeploys, and asserts that: - The column is no longer in the
- * database
- *
- * Alters the name of one of the columns, redeploys, and asserts that: - The old column is
- * dropped correctly - The new column is added correctly
- *
- * Alters the type one of the columns, redeploys, and asserts that: - The old column is dropped
- * correctly - The new column is added correctly
- *
- * Alters the name and type of one of the columns, redeploys, and asserts that: - The old column
- * is dropped correctly - The new column is added correctly
+ * Replaced by EncryptionAtRestTest: a channel with encryptData set stores every content stage
+ * as ciphertext, and the decrypting read path hands the original back.
*/
- @Test
- public final void testUpdateMetaDataColumns() throws Exception {
- ChannelController.getInstance().removeChannel(channelId);
-
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
-
- channel.deploy();
-
- // Assert that there are no columns currently
- assertEquals(TestUtils.getExistingMetaDataColumns(channelId).size(), 0);
-
- // Add all the columns
- channel.getMetaDataColumns().add(new MetaDataColumn("stringcolumn", MetaDataColumnType.STRING, null));
- channel.getMetaDataColumns().add(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null));
- channel.getMetaDataColumns().add(new MetaDataColumn("booleancolumn", MetaDataColumnType.BOOLEAN, null));
- channel.getMetaDataColumns().add(new MetaDataColumn("timestampcolumn", MetaDataColumnType.TIMESTAMP, null));
-
- channel.undeploy();
- channel.deploy();
-
- // Assert that each column exists
- List columns = TestUtils.getExistingMetaDataColumns(channelId);
- assertTrue(columns.contains(new MetaDataColumn("stringcolumn", MetaDataColumnType.STRING, null)));
- assertTrue(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null)));
- assertTrue(columns.contains(new MetaDataColumn("booleancolumn", MetaDataColumnType.BOOLEAN, null)));
- assertTrue(columns.contains(new MetaDataColumn("timestampcolumn", MetaDataColumnType.TIMESTAMP, null)));
-
- // Remove the string column
- channel.getMetaDataColumns().remove(0);
-
- channel.undeploy();
- channel.deploy();
-
- // Assert that the string column doesn't exist anymore
- columns = TestUtils.getExistingMetaDataColumns(channelId);
- assertFalse(columns.contains(new MetaDataColumn("stringcolumn", MetaDataColumnType.STRING, null)));
-
- // Alter the long column's name
- channel.getMetaDataColumns().get(0).setName("longcolumn2");
-
- channel.undeploy();
- channel.deploy();
-
- // Assert that the long column got dropped/added correctly
- columns = TestUtils.getExistingMetaDataColumns(channelId);
- assertFalse(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null)));
- assertTrue(columns.contains(new MetaDataColumn("numbercolumn2", MetaDataColumnType.NUMBER, null)));
-
- // Alter the double column's type
- channel.getMetaDataColumns().get(1).setType(MetaDataColumnType.TIMESTAMP);
-
- channel.undeploy();
- channel.deploy();
-
- // Assert that the double column got dropped/added correctly as a timestamp column
- columns = TestUtils.getExistingMetaDataColumns(channelId);
- assertFalse(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null)));
- assertTrue(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.TIMESTAMP, null)));
-
- // Alter the boolean column's name and type
- channel.getMetaDataColumns().get(2).setName("booleancolumn2");
- channel.getMetaDataColumns().get(2).setType(MetaDataColumnType.TIMESTAMP);
-
- channel.undeploy();
- channel.deploy();
-
- // Assert that the boolean column got dropped/added correctly as a time column
- columns = TestUtils.getExistingMetaDataColumns(channelId);
- assertFalse(columns.contains(new MetaDataColumn("booleancolumn", MetaDataColumnType.BOOLEAN, null)));
- assertTrue(columns.contains(new MetaDataColumn("booleancolumn2", MetaDataColumnType.TIMESTAMP, null)));
-
- channel.undeploy();
- }
-
- @Test
- public final void testMetaDataCasting() throws MetaDataColumnException {
- MetaDataColumnType columnType = MetaDataColumnType.BOOLEAN;
- Boolean booleanValue = (Boolean) columnType.castValue("TRUE");
- assertEquals(Boolean.TRUE, booleanValue);
- booleanValue = (Boolean) columnType.castValue("FALSE");
- assertEquals(Boolean.FALSE, booleanValue);
-
- columnType = MetaDataColumnType.NUMBER;
- BigDecimal bigDecimalValue = (BigDecimal) columnType.castValue("1.0234567890123456789");
- assertEquals(new BigDecimal(1.0234567890123456789), bigDecimalValue);
-
- columnType = MetaDataColumnType.STRING;
- String stringValue = (String) columnType.castValue(" test !@# String 123 ");
- assertEquals(" test !@# String 123 ", stringValue);
-
- columnType = MetaDataColumnType.TIMESTAMP;
- Calendar dateValue = (Calendar) columnType.castValue("2010-01-02 13:01:02");
- assertEquals("13 01 02 01 02 2010", new SimpleDateFormat("HH mm ss MM dd yyyy").format(dateValue.getTimeInMillis()));
- }
/*
- * Create a new test channel Process a source message with a metadata ID of 1, assert that: - An
- * InvalidConnectorMessageState exception is thrown
- *
- * Process a source message with a status other than RECEIVED, assert that: - An
- * InvalidConnectorMessageState exception is thrown
- *
- * Process a valid source message, and assert that: - The pre-processor was run - The processed
- * raw content was stored - The filter/transformer was run - The transformed/encoded content was
- * stored - Initial messages were created for each destination chain - The message processed
- * through at least the first destination connector for each chain - The post-processor was run
- * - The final transaction was created
+ * Replaced by the ci/tests/200-custom-metadata-columns fixtures (a column of each type,
+ * value casting, and values that are absent or uncastable) and
+ * CustomMetaDataColumnRedeployTest (adding, removing and retyping a deployed channel's
+ * columns, which is what testUpdateMetaDataColumns redeployed for).
*/
- @Test
- public final void testProcess() throws Exception {
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
-
- channel.deploy();
- channel.start(null);
-
- ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channelId, channelName, serverId).getConnectorMessages().get(0);
-
- Message message = null;
-
- message = channel.process(sourceMessage, false);
-
- // Assert that the message was run through the pre-processor
- assertTrue(((TestPreProcessor) channel.getPreProcessor()).isProcessed());
-
- // Assert that the processed raw content was stored
- TestUtils.assertMessageContentExists(sourceMessage.getProcessedRaw());
-
- // Assert that the FilterTransformer was run
- assertTrue(((TestFilterTransformer) channel.getSourceConnector().getFilterTransformerExecutor().getFilterTransformer()).isTransformed());
-
- // Assert that the transformed/encoded content was stored
- TestUtils.assertMessageContentExists(sourceMessage.getTransformed());
- TestUtils.assertMessageContentExists(sourceMessage.getEncoded());
-
- for (DestinationChainProvider chain : channel.getDestinationChainProviders()) {
- Integer firstId = null;
- for (Integer metaDataId : chain.getDestinationConnectors().keySet()) {
- if (firstId == null || metaDataId < firstId) {
- firstId = metaDataId;
- }
- }
- // Assert that messages were created for each destination chain
- TestUtils.assertConnectorMessageExists(message.getConnectorMessages().get(firstId), false);
- // Assert that the message processed through at least the first destination connector for each chain
- assertTrue(((TestDestinationConnector) chain.getDestinationConnectors().get(firstId)).getMessageIds().size() > 0);
- }
-
- // Assert that the message was run through the post-processor
- assertTrue(((TestPostProcessor) channel.getPostProcessor()).isProcessed());
-
- channel.stop();
- channel.undeploy();
- }
-
- @Test
- public final void testEncryption() throws Exception {
- //TODO UPDATE THIS TEST!
-// final String prefix = "Encrypted: ";
-// final int prefixLength = prefix.length();
-//
-// TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
-// channel.setEncryptor(new Encryptor() {
-// @Override
-// public String encrypt(String text) {
-// return prefix + text;
-// }
-//
-// @Override
-// public String decrypt(String text) {
-// return text.substring(prefixLength);
-// }
-// });
-//
-// SourceConnector sourceConnector = channel.getSourceConnector();
-//
-// channel.deploy();
-// channel.start();
-//
-// DispatchResult dispatchResult = sourceConnector.dispatchRawMessage(new RawMessage(testMessage));
-// sourceConnector.finishDispatch(dispatchResult);
-//
-// channel.stop();
-// channel.undeploy();
-//
-// Connection connection = null;
-// PreparedStatement statement = null;
-// ResultSet resultSet = null;
-//
-// try {
-// connection = TestUtils.getConnection();
-//
-// long messageId = dispatchResult.getProcessedMessage().getMessageId();
-//
-// statement = connection.prepareStatement("SELECT content, is_encrypted FROM d_mc" + ChannelController.getInstance().getLocalChannelId(channelId) + " WHERE message_id = ? AND metadata_id = ? AND content_type = ?");
-// statement.setLong(1, messageId);
-//
-// for (ConnectorMessage connectorMessage : dispatchResult.getProcessedMessage().getConnectorMessages().values()) {
-// int metaDataId = connectorMessage.getMetaDataId();
-// statement.setInt(2, metaDataId);
-//
-// for (ContentType contentType : ContentType.getMessageTypes()) {
-// MessageContent messageContent = connectorMessage.getContent(contentType);
-//
-// if (messageContent != null) {
-// assertNotNull(messageContent.getContent());
-// //TODO Update this test, no longer valid
-//// assertEquals(prefix + messageContent.getContent(), messageContent.getEncryptedContent());
-// }
-//
-// statement.setInt(3, contentType.getContentTypeCode());
-// resultSet = statement.executeQuery();
-//
-// if (resultSet.next()) {
-// assertEquals(prefix + messageContent.getContent(), resultSet.getString("content"));
-// assertTrue(resultSet.getBoolean("is_encrypted"));
-// } else if (messageContent != null && (metaDataId == 0 || !contentType.equals(ContentType.RAW))) {
-// throw new AssertionError("Message content was not stored in the database (" + messageId + "/" + metaDataId + "/" + contentType.getContentTypeCode() + ")");
-// }
-//
-// resultSet.close();
-// }
-// }
-// } finally {
-// TestUtils.close(resultSet);
-// TestUtils.close(statement);
-// TestUtils.close(connection);
-// }
- }
- @Test
- public final void testContentRemoval() throws Exception {
- testContentRemoval(false, false);
- testContentRemoval(true, false);
- }
-
- @Test
- public final void testContentRemovalWithQueueing() throws Exception {
- testContentRemoval(false, true);
- testContentRemoval(true, true);
- }
-
- private void testContentRemoval(boolean removeContentOnCompletion, boolean useQueue) throws Exception {
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
- channel.getStorageSettings().setRemoveContentOnCompletion(removeContentOnCompletion);
-
- if (useQueue) {
- DestinationConnectorProperties destinationConnectorProperties = ((DestinationConnectorPropertiesInterface) channel.getDestinationConnector(1).getConnectorProperties()).getDestinationConnectorProperties();
- destinationConnectorProperties.setQueueEnabled(true);
- destinationConnectorProperties.setSendFirst(false);
- }
-
- SourceConnector sourceConnector = channel.getSourceConnector();
-
- channel.deploy();
- channel.start(null);
-
- DispatchResult dispatchResult = sourceConnector.dispatchRawMessage(new RawMessage(testMessage));
- sourceConnector.finishDispatch(dispatchResult);
-
- // if queueing, give the queue time to flush out
- if (useQueue) {
- Thread.sleep(1000);
- }
-
- channel.stop();
- channel.undeploy();
-
- for (ConnectorMessage connectorMessage : dispatchResult.getProcessedMessage().getConnectorMessages().values()) {
- boolean foundContent = false;
-
- for (ContentType contentType : ContentType.getMessageTypes()) {
- MessageContent messageContent = connectorMessage.getMessageContent(contentType);
-
- if (messageContent != null && (messageContent.getMetaDataId() == 0 || messageContent.getContentType() != ContentType.RAW)) {
- foundContent = true;
-
- if (removeContentOnCompletion) {
- TestUtils.assertMessageContentDoesNotExist(messageContent);
- } else {
- TestUtils.assertMessageContentExists(messageContent);
- }
- }
- }
-
- assertTrue(foundContent);
- }
- }
-
- @Test
- public final void testContentStorageDevelopment() throws Exception {
- testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.DEVELOPMENT));
- }
-
- @Test
- public final void testContentStorageProduction() throws Exception {
- testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.PRODUCTION));
- }
-
- @Test
- public final void testContentStorageMetadata() throws Exception {
- testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.METADATA));
- }
-
- @Test
- public final void testContentStorageDisabled() throws Exception {
- testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.DISABLED));
- }
-
- private void testContentStorageSettings(StorageSettings storageSettings) throws Exception {
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
- channel.setStorageSettings(storageSettings);
- SourceConnector sourceConnector = channel.getSourceConnector();
-
- channel.deploy();
- channel.start(null);
-
- DispatchResult dispatchResult = sourceConnector.dispatchRawMessage(new RawMessage(testMessage));
- sourceConnector.finishDispatch(dispatchResult);
-
- channel.stop();
- channel.undeploy();
-
- ConnectorMessage sourceMessage = dispatchResult.getProcessedMessage().getConnectorMessages().get(0);
- ConnectorMessage destinationMessage = dispatchResult.getProcessedMessage().getConnectorMessages().get(1);
-
- assertNotNull(sourceMessage);
- assertNotNull(destinationMessage);
-
- if (storageSettings.isStoreRaw()) {
- TestUtils.assertMessageContentExists(sourceMessage.getRaw());
- } else {
- TestUtils.assertMessageContentDoesNotExist(sourceMessage.getRaw());
- }
-
- if (storageSettings.isStoreProcessedRaw()) {
- TestUtils.assertMessageContentExists(sourceMessage.getProcessedRaw());
- } else {
- TestUtils.assertMessageContentDoesNotExist(sourceMessage.getProcessedRaw());
- }
-
- if (storageSettings.isStoreTransformed()) {
- TestUtils.assertMessageContentExists(sourceMessage.getTransformed());
- TestUtils.assertMessageContentExists(destinationMessage.getTransformed());
- } else {
- TestUtils.assertMessageContentDoesNotExist(sourceMessage.getTransformed());
- TestUtils.assertMessageContentDoesNotExist(destinationMessage.getTransformed());
- }
-
- if (storageSettings.isStoreSourceEncoded()) {
- TestUtils.assertMessageContentExists(sourceMessage.getEncoded());
- } else {
- TestUtils.assertMessageContentDoesNotExist(sourceMessage.getEncoded());
- }
-
- if (storageSettings.isStoreSent()) {
- TestUtils.assertMessageContentExists(destinationMessage.getSent());
- } else {
- TestUtils.assertMessageContentDoesNotExist(new MessageContent(channelId, dispatchResult.getMessageId(), 1, ContentType.SENT, null, null, false));
- }
-
- if (storageSettings.isStoreResponse()) {
- TestUtils.assertMessageContentExists(destinationMessage.getResponse());
- } else {
- TestUtils.assertMessageContentDoesNotExist(destinationMessage.getResponse());
- }
-
- if (storageSettings.isStoreResponseTransformed()) {
- TestUtils.assertMessageContentExists(destinationMessage.getResponseTransformed());
- } else {
- TestUtils.assertMessageContentDoesNotExist(destinationMessage.getResponseTransformed());
- }
-
- if (storageSettings.isStoreProcessedResponse()) {
- TestUtils.assertMessageContentExists(destinationMessage.getProcessedResponse());
- } else {
- TestUtils.assertMessageContentDoesNotExist(destinationMessage.getProcessedResponse());
- }
- }
+ /*
+ * Replaced by the ci/tests/160-message-storage-levels fixtures (DEVELOPMENT, PRODUCTION, RAW
+ * and METADATA), MessageStorageDisabledTest (DISABLED) and the ci/tests/170-content-removal
+ * fixtures (removeContentOnCompletion, with and without a queued destination).
+ */
}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationChainTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationChainTests.java
deleted file mode 100644
index 903f94d8b6..0000000000
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationChainTests.java
+++ /dev/null
@@ -1,264 +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.donkey.test;
-
-import static org.junit.Assert.assertTrue;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.util.Map;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-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.Response;
-import com.mirth.connect.donkey.model.message.Status;
-import com.mirth.connect.donkey.server.Donkey;
-import com.mirth.connect.donkey.server.StartException;
-import com.mirth.connect.donkey.server.channel.Channel;
-import com.mirth.connect.donkey.server.channel.DestinationChainProvider;
-import com.mirth.connect.donkey.server.channel.DispatchResult;
-import com.mirth.connect.donkey.server.channel.FilterTransformerExecutor;
-import com.mirth.connect.donkey.server.channel.FilterTransformerResult;
-import com.mirth.connect.donkey.server.channel.MetaDataReplacer;
-import com.mirth.connect.donkey.server.channel.components.FilterTransformerException;
-import com.mirth.connect.donkey.server.controllers.ChannelController;
-import com.mirth.connect.donkey.test.util.TestChannel;
-import com.mirth.connect.donkey.test.util.TestConnectorProperties;
-import com.mirth.connect.donkey.test.util.TestDataType;
-import com.mirth.connect.donkey.test.util.TestDestinationConnector;
-import com.mirth.connect.donkey.test.util.TestFilterTransformer;
-import com.mirth.connect.donkey.test.util.TestPostProcessor;
-import com.mirth.connect.donkey.test.util.TestPreProcessor;
-import com.mirth.connect.donkey.test.util.TestResponseTransformer;
-import com.mirth.connect.donkey.test.util.TestSourceConnector;
-import com.mirth.connect.donkey.test.util.TestUtils;
-
-public class DestinationChainTests {
- private static int TEST_SIZE = 10;
- private static String channelId = TestUtils.DEFAULT_CHANNEL_ID;
- private static String serverId = TestUtils.DEFAULT_SERVER_ID;
- private static String testMessage = TestUtils.TEST_HL7_MESSAGE;
- private Logger logger = LogManager.getLogger(this.getClass());
-
- @BeforeClass
- final public static void beforeClass() throws StartException {
- Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration());
- }
-
- @AfterClass
- final public static void afterClass() throws StartException {
- Donkey.getInstance().stopEngine();
- }
-
- /*
- * Create channel with two destination chains, two destination connectors each Set each
- * destination connector's FilterTransformer to place values in the connector, channel, and
- * response maps
- *
- * Send messages, and for each destination connector, assert that: - The transformed data was
- * stored - The encoded data was stored - The connector message maps were all updated correctly
- * - The connector message status was updated
- */
- @Test
- public final void testStoreData() throws Exception {
- int numChains = 2;
- int numDestinationsPerChain = 2;
- long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId);
-
- TestChannel channel = new TestChannel();
-
- channel.setChannelId(channelId);
- channel.setServerId(serverId);
-
- channel.setPreProcessor(new TestPreProcessor());
- channel.setPostProcessor(new TestPostProcessor());
-
- TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector();
- sourceConnector.setRespondAfterProcessing(true);
- sourceConnector.setChannelId(channel.getChannelId());
- sourceConnector.setChannel(channel);
- sourceConnector.setMetaDataReplacer(new MetaDataReplacer());
-
- channel.setSourceConnector(sourceConnector);
- channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
-
- class TestFilterTransformer2 extends TestFilterTransformer {
- @Override
- public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException {
- // Alter the connector message maps
- message.getConnectorMap().put("key", "value");
- message.getChannelMap().put("key", "value");
- message.getResponseMap().put("key", new Response(Status.SENT, "value"));
- return super.doFilterTransform(message);
- }
- }
-
- for (int i = 1; i <= numChains; i++) {
- DestinationChainProvider chain = new DestinationChainProvider();
- chain.setChannelId(channel.getChannelId());
-
- for (int j = 1; j <= numDestinationsPerChain; j++) {
- int metaDataId = (i - 1) * numDestinationsPerChain + j;
- TestDestinationConnector destinationConnector = (TestDestinationConnector) TestUtils.createDestinationConnector(channel.getChannelId(), channel.getServerId(), new TestConnectorProperties(), TestUtils.DEFAULT_DESTINATION_NAME, new TestDataType(), new TestDataType(), new TestResponseTransformer(), metaDataId);
- destinationConnector.setChannelId(channel.getChannelId());
-
- destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer());
- destinationConnector.setMetaDataColumns(channel.getMetaDataColumns());
-
- FilterTransformerExecutor filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType());
- filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer2());
-
- destinationConnector.setFilterTransformerExecutor(filterTransformerExecutor);
-
- chain.addDestination(metaDataId, destinationConnector);
- }
-
- channel.addDestinationChainProvider(chain);
- }
-
- channel.deploy();
- channel.start(null);
-
- if (ChannelController.getInstance().channelExists(channelId)) {
- ChannelController.getInstance().deleteAllMessages(channelId);
- }
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- DispatchResult messageResponse = ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
-
- for (DestinationChainProvider chain : channel.getDestinationChainProviders()) {
- for (int metaDataId : chain.getDestinationConnectors().keySet()) {
- Connection connection = null;
- PreparedStatement statement = null;
- ResultSet result = null;
-
- try {
- connection = TestUtils.getConnection();
-
- // Assert that the transformed data was stored
- statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId + " WHERE message_id = ? AND metadata_id = ? AND content_type = ?");
- statement.setLong(1, messageResponse.getMessageId());
- statement.setInt(2, metaDataId);
- statement.setInt(3, ContentType.TRANSFORMED.getContentTypeCode());
- result = statement.executeQuery();
- assertTrue(result.next());
- TestUtils.close(result);
-
- // Assert that the encoded data was stored
- statement.setInt(3, ContentType.ENCODED.getContentTypeCode());
- result = statement.executeQuery();
- assertTrue(result.next());
- } finally {
- TestUtils.close(result);
- TestUtils.close(statement);
- TestUtils.close(connection);
- }
-
- // Assert that the connector message maps were updated
- Map connectorMap = TestUtils.getConnectorMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId);
- Map channelMap = TestUtils.getChannelMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId);
- Map responseMap = TestUtils.getResponseMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId);
- assertTrue(connectorMap.get("key").equals("value"));
- assertTrue(channelMap.get("key").equals("value"));
- assertTrue(responseMap.get("key").equals(new Response(Status.SENT, "value")));
-
- // Assert that the connector message status was updated
- TestUtils.assertConnectorMessageStatusEquals(channel.getChannelId(), messageResponse.getMessageId(), metaDataId, Status.SENT);
- }
- }
- }
-
- channel.stop();
- channel.undeploy();
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Create channel with two destination chains, two destination connectors each Set the source
- * FilterTransformer to place values in the channel and response maps
- *
- * Send messages, and for each destination connector, assert that: - The connector message was
- * stored - The channel and response maps were updated correctly - If the destination connector
- * isn't the first one in the chain, the raw data was stored - If the destination connector is
- * the first one in the chain, the source encoded data was stored
- */
- @Test
- public final void testCreateNextMessage() throws Exception {
- long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId);
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId, true, 2, 2);
-
- channel.getSourceConnector().getFilterTransformerExecutor().setFilterTransformer(new TestFilterTransformer() {
- @Override
- public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException {
- // Alter the channel and response maps
- message.getChannelMap().put("key", "value");
- message.getResponseMap().put("key", new Response(Status.SENT, "value"));
- return new FilterTransformerResult(false, null);
- }
- });
-
- channel.deploy();
- channel.start(null);
- Connection connection = null;
- PreparedStatement statement = null;
- ResultSet result = null;
-
- try {
- connection = TestUtils.getConnection();
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- DispatchResult messageResponse = ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
-
- for (DestinationChainProvider chain : channel.getDestinationChainProviders()) {
- for (int metaDataId : chain.getMetaDataIds()) {
- // Assert that the connector message was stored
- statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId + " WHERE message_id = ? AND id = ?");
- statement.setLong(1, messageResponse.getMessageId());
- statement.setInt(2, metaDataId);
- result = statement.executeQuery();
- assertTrue(result.next());
- result.close();
- statement.close();
-
- // Assert that the channel and response maps were updated
- Map channelMap = TestUtils.getChannelMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId);
- Map responseMap = TestUtils.getResponseMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId);
- assertTrue(channelMap.get("key").equals("value"));
- assertTrue(responseMap.get("key").equals(new Response(Status.SENT, "value")));
-
- // Assert that the raw data was stored
- statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId + " WHERE message_id = ? AND metadata_id = 0 AND content_type = ?");
- statement.setLong(1, messageResponse.getMessageId());
- statement.setInt(2, ContentType.ENCODED.getContentTypeCode());
- result = statement.executeQuery();
- assertTrue(result.next());
- result.close();
- statement.close();
- }
- }
- }
- } finally {
- TestUtils.close(result);
- TestUtils.close(statement);
- TestUtils.close(connection);
- }
-
- channel.stop();
- channel.undeploy();
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java
index 1bac478ccd..a06ad092ed 100644
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java
+++ b/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java
@@ -16,26 +16,18 @@
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
-import java.util.Map;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
-import com.mirth.connect.donkey.model.DonkeyException;
import com.mirth.connect.donkey.model.channel.ConnectorProperties;
import com.mirth.connect.donkey.model.channel.DeployedState;
import com.mirth.connect.donkey.model.channel.DestinationConnectorProperties;
-import com.mirth.connect.donkey.model.channel.DestinationConnectorPropertiesInterface;
-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.RawMessage;
-import com.mirth.connect.donkey.model.message.Response;
import com.mirth.connect.donkey.model.message.Status;
import com.mirth.connect.donkey.server.Donkey;
import com.mirth.connect.donkey.server.StartException;
-import com.mirth.connect.donkey.server.channel.Channel;
import com.mirth.connect.donkey.server.channel.DestinationChainProvider;
import com.mirth.connect.donkey.server.channel.DestinationConnector;
import com.mirth.connect.donkey.server.channel.DispatchResult;
@@ -47,10 +39,8 @@
import com.mirth.connect.donkey.test.util.TestDispatcherProperties;
import com.mirth.connect.donkey.test.util.TestPostProcessor;
import com.mirth.connect.donkey.test.util.TestPreProcessor;
-import com.mirth.connect.donkey.test.util.TestResponseTransformer;
import com.mirth.connect.donkey.test.util.TestSourceConnector;
import com.mirth.connect.donkey.test.util.TestUtils;
-import com.mirth.connect.donkey.util.Serializer;
public class DestinationConnectorTests {
private static int TEST_SIZE = 10;
@@ -364,227 +354,8 @@ private void testProcess(boolean queueNull, boolean queueEnabled, boolean queueS
}
/*
- * Create channel where the response transformer blocks the thread Send messages in asynchronous
- * thread (so that the response transformer is waiting), assert that: - The destination
- * connector response content was stored - The message status was updated to PENDING in the
- * database
- *
- * Then allow the response transformer to finish, join the thread, and assert: - The response
- * transformer was successfully run - The message status was updated to SENT in the database
- */
- @Test
- public final void testAfterSend() throws Exception {
- ChannelController.getInstance().getLocalChannelId(channelId);
-
- final TestChannel channel = new TestChannel();
-
- channel.setChannelId(channelId);
- channel.setServerId(serverId);
-
- channel.setPreProcessor(new TestPreProcessor());
- channel.setPostProcessor(new TestPostProcessor());
-
- final TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector();
- sourceConnector.setChannelId(channel.getChannelId());
- sourceConnector.setChannel(channel);
- channel.setSourceConnector(sourceConnector);
- channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
-
- final ConnectorProperties connectorProperties = new TestDispatcherProperties();
- ((TestDispatcherProperties) connectorProperties).getDestinationConnectorProperties().setQueueEnabled(true);
- ((TestDispatcherProperties) connectorProperties).getDestinationConnectorProperties().setSendFirst(true);
- ((TestDispatcherProperties) connectorProperties).getDestinationConnectorProperties().setRegenerateTemplate(true);
-
- final DestinationConnector destinationConnector = new TestDispatcher();
- TestUtils.initDefaultDestinationConnector(destinationConnector, connectorProperties);
- destinationConnector.setChannelId(channelId);
- ((TestDispatcher) destinationConnector).setReturnStatus(Status.SENT);
-
- class BlockingTestResponseTransformer extends TestResponseTransformer {
- public volatile boolean waiting = true;
-
- @Override
- public String doTransform(Response response, ConnectorMessage connectorMessage) throws DonkeyException, InterruptedException {
- while (waiting) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- return super.doTransform(response, connectorMessage);
- }
- }
- final BlockingTestResponseTransformer responseTransformer = new BlockingTestResponseTransformer();
-
- destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer());
- destinationConnector.setMetaDataColumns(channel.getMetaDataColumns());
- destinationConnector.setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
- destinationConnector.setResponseTransformerExecutor(TestUtils.createDefaultResponseTransformerExecutor());
- destinationConnector.getResponseTransformerExecutor().setResponseTransformer(responseTransformer);
-
- DestinationChainProvider chain = new DestinationChainProvider();
- chain.setChannelId(channelId);
- chain.addDestination(1, destinationConnector);
- channel.addDestinationChainProvider(chain);
-
- if (ChannelController.getInstance().channelExists(channelId)) {
- ChannelController.getInstance().deleteAllMessages(channelId);
- }
-
- channel.deploy();
- channel.start(null);
-
- class TempClass {
- public long messageId;
- }
- final TempClass tempClass = new TempClass();
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- responseTransformer.waiting = true;
-
- Thread thread = new Thread() {
- @Override
- public void run() {
- ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId()).getConnectorMessages().get(0);
- tempClass.messageId = sourceMessage.getMessageId();
-
- try {
- channel.process(sourceMessage, false);
- } catch (InterruptedException e) {
- throw new AssertionError(e);
- }
- }
- };
- thread.start();
-
- Thread.sleep(100);
- // Assert that the response content was stored
- Connection connection = null;
- PreparedStatement statement = null;
- ResultSet result = null;
-
- try {
- connection = TestUtils.getConnection();
- long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId);
- statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId + " WHERE message_id = ? AND metadata_id = ? AND content_type = ?");
- statement.setLong(1, tempClass.messageId);
- statement.setInt(2, 1);
- statement.setInt(3, ContentType.SENT.getContentTypeCode());
- result = statement.executeQuery();
- assertTrue(result.next());
- result.close();
- statement.close();
-
- // Assert that the message status was updated to PENDING
- statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId + " WHERE message_id = ? AND id = ? AND status = ?");
- statement.setLong(1, tempClass.messageId);
- statement.setInt(2, 1);
- statement.setString(3, String.valueOf(Status.PENDING.getStatusCode()));
- result = statement.executeQuery();
- assertTrue(result.next());
- result.close();
- statement.close();
-
- responseTransformer.waiting = false;
- thread.join();
-
- // Assert that the response transformer was run
- assertTrue(responseTransformer.isTransformed());
-
- // Assert that the message status was updated to SENT
- statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId + " WHERE message_id = ? AND id = ? AND status = ?");
- statement.setLong(1, tempClass.messageId);
- statement.setInt(2, 1);
- statement.setString(3, String.valueOf(Status.SENT.getStatusCode()));
- result = statement.executeQuery();
- assertTrue(result.next());
- result.close();
- statement.close();
- } finally {
- TestUtils.close(result);
- TestUtils.close(statement);
- TestUtils.close(connection);
- }
- }
-
- channel.stop();
- channel.undeploy();
- //ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Create new channel where the response transformer changes the message and status of the
- * Response object If the response status was changed to QUEUED and queuing is not enabled, or
- * if the status was changed to something invalid (RECEIVED/TRANSFORMED/PENDING), then assume
- * that it was changed to ERROR
- *
- * Send messages, assert that: - The processed response was stored - The destination entry in
- * the response map was overwritten - The connector message status was changed based on the
- * response status
- *
- * Do the above steps for all statuses
+ * testAfterSend and testRunResponseTransformer are re-implemented against the CI harness:
+ * ci/tests/190-response-handling for the response transformer, and
+ * smoketest BlockingResponseTransformerTest for a transformer that has not returned yet.
*/
- @Test
- public final void testRunResponseTransformer() throws Exception {
- for (Status status : Status.values()) {
- testRunResponseTransformer(status);
- }
- }
-
- private void testRunResponseTransformer(Status responseStatus) throws Exception {
- final Response testResponse = new Response(responseStatus, TestUtils.TEST_HL7_ACK);
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- Response finalResponse = new Response(testResponse.getStatus(), testResponse.getMessage());
- if (finalResponse.getStatus() != Status.ERROR && finalResponse.getStatus() != Status.SENT && finalResponse.getStatus() != Status.QUEUED) {
- // If the response is invalid for a final destination finalResponse.getStatus(), change the status to ERROR
- finalResponse.setStatus(Status.ERROR);
- } else if (channel.getDestinationConnector(1).getConnectorProperties() instanceof DestinationConnectorPropertiesInterface) {
- // If the destination connector isn't queuing, and the response status is QUEUED, then it should have changed to ERROR
- DestinationConnectorProperties destinationConnectorProperties = ((DestinationConnectorPropertiesInterface) channel.getDestinationConnector(1).getConnectorProperties()).getDestinationConnectorProperties();
- if ((destinationConnectorProperties == null || !destinationConnectorProperties.isQueueEnabled()) && finalResponse.getStatus() == Status.QUEUED) {
- finalResponse.setStatus(Status.ERROR);
- }
- } else if (finalResponse.getStatus() == Status.QUEUED) {
- // If the destination connector isn't queuing, and the response status is QUEUED, then it should have changed to ERROR
- finalResponse.setStatus(Status.ERROR);
- }
-
- class TestResponseTransformer2 extends TestResponseTransformer {
- @Override
- public String doTransform(Response response, ConnectorMessage connectorMessage) throws DonkeyException, InterruptedException {
- response.setMessage(testResponse.getMessage());
- response.setStatus(testResponse.getStatus());
- connectorMessage.getResponseTransformed().setContent(testResponse.getMessage());
- return super.doTransform(response, connectorMessage);
- }
- }
- channel.getDestinationConnector(1).getResponseTransformerExecutor().setResponseTransformer(new TestResponseTransformer2());
-
- //ChannelController.getInstance().deleteAllMessages(channel.getChannelId());
- channel.deploy();
- channel.start(null);
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- DispatchResult messageResponse = ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
- Serializer serializer = Donkey.getInstance().getSerializer();
- String responseString = serializer.serialize(finalResponse);
-
- // Assert that the processed response was stored
- MessageContent messageContent = new MessageContent(channel.getChannelId(), messageResponse.getMessageId(), 1, ContentType.PROCESSED_RESPONSE, responseString, null, false);
- TestUtils.assertMessageContentExists(messageContent);
-
- // Assert that the entry in the response map was overwritten
- Map responseMap = TestUtils.getResponseMap(channel.getChannelId(), messageResponse.getMessageId(), 1);
- assertTrue(responseMap.get("d1").equals(finalResponse));
-
- // Assert that the message status was changed
- TestUtils.assertConnectorMessageStatusEquals(channel.getChannelId(), messageResponse.getMessageId(), 1, finalResponse.getStatus());
- }
-
- channel.stop();
- channel.undeploy();
- //ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java
index c7a4f8569c..087fa24a22 100644
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java
+++ b/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java
@@ -23,7 +23,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -32,8 +31,6 @@
import org.junit.BeforeClass;
import org.junit.Test;
-import com.mirth.connect.donkey.model.channel.MetaDataColumn;
-import com.mirth.connect.donkey.model.channel.MetaDataColumnType;
import com.mirth.connect.donkey.model.message.ConnectorMessage;
import com.mirth.connect.donkey.model.message.ContentType;
import com.mirth.connect.donkey.model.message.Message;
@@ -41,12 +38,10 @@
import com.mirth.connect.donkey.model.message.RawMessage;
import com.mirth.connect.donkey.model.message.Response;
import com.mirth.connect.donkey.model.message.Status;
-import com.mirth.connect.donkey.model.message.attachment.Attachment;
import com.mirth.connect.donkey.server.Donkey;
import com.mirth.connect.donkey.server.StartException;
import com.mirth.connect.donkey.server.channel.Channel;
import com.mirth.connect.donkey.server.channel.DestinationChainProvider;
-import com.mirth.connect.donkey.server.channel.DispatchResult;
import com.mirth.connect.donkey.server.channel.StorageSettings;
import com.mirth.connect.donkey.server.controllers.ChannelController;
import com.mirth.connect.donkey.server.data.DonkeyDao;
@@ -324,103 +319,8 @@ public final void testInsertMessageContent() throws Exception {
// TODO testBatchInsertMessageContent
- /*
- * Create new channel and message Insert attachments for the message, assert that: - The
- * attachment was inserted correctly
- */
- @Test
- public final void testInsertMessageAttachment() throws Exception {
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
- DonkeyDao dao = daoFactory.getDao();
-
- try {
- logger.info("Testing DonkeyDao.insertMessageAttachment...");
-
- ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0);
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- Attachment attachment = new Attachment("attachment" + i, testMessage.getBytes(), "text/plain");
-
- dao.insertMessageAttachment(channel.getChannelId(), sourceMessage.getMessageId(), attachment);
- dao.commit();
-
- // Assert that the attachment was inserted
- TestUtils.assertAttachmentExists(channel.getChannelId(), sourceMessage.getMessageId(), attachment);
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- dao.close();
- }
- }
-
- /*
- * Create a new channel, and create metadata columns for it Deploy the channel, assert that: -
- * The columns were successfully added to the table
- *
- * Create source connector messages Insert source/destination metadata for each message, assert
- * that: - Each of the metadata columns was inserted successfully
- */
- @Test
- public final void testInsertMetaData() throws Exception {
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- for (MetaDataColumnType columnType : MetaDataColumnType.values()) {
- channel.getMetaDataColumns().add(new MetaDataColumn(columnType.toString() + "test", columnType, null));
- }
-
- channel.deploy();
-
- // Assert that the columns were added successfully
- List existingColumns = TestUtils.getExistingMetaDataColumns(channel.getChannelId());
- List channelColumns = channel.getMetaDataColumns();
-
- assertEquals(channelColumns.size(), existingColumns.size());
-
- for (MetaDataColumn metaDataColumn : channelColumns) {
- assertTrue(metaDataColumn.getName(), existingColumns.contains(metaDataColumn));
- }
-
- Map sourceMap = new HashMap();
- sourceMap.put(MetaDataColumnType.BOOLEAN.toString() + "test", true);
- sourceMap.put(MetaDataColumnType.NUMBER.toString() + "test", 1);
- sourceMap.put(MetaDataColumnType.STRING.toString() + "test", "testing");
- sourceMap.put(MetaDataColumnType.TIMESTAMP.toString() + "test", Calendar.getInstance());
-
- Map destinationMap = new HashMap();
- destinationMap.put(MetaDataColumnType.BOOLEAN.toString() + "test", false);
- destinationMap.put(MetaDataColumnType.NUMBER.toString() + "test", 1);
- destinationMap.put(MetaDataColumnType.STRING.toString() + "test", "");
- destinationMap.put(MetaDataColumnType.TIMESTAMP.toString() + "test", Calendar.getInstance());
-
- logger.info("Testing DonkeyDao.insertMetaData...");
-
- DonkeyDao dao = daoFactory.getDao();
-
- try {
- for (int i = 1; i <= TEST_SIZE; i++) {
- ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0);
-
- sourceMessage.setMetaDataMap(sourceMap);
- dao.insertMetaData(sourceMessage, channel.getMetaDataColumns());
- dao.commit();
-
- ConnectorMessage destinationMessage = TestUtils.createAndStoreDestinationConnectorMessage(daoFactory, channel.getChannelId(), channel.getName(), channel.getServerId(), sourceMessage.getMessageId(), 1, testMessage, Status.RECEIVED);
- destinationMessage.setMetaDataMap(destinationMap);
- dao.insertMetaData(destinationMessage, channel.getMetaDataColumns());
- dao.commit();
-
- // Assert the custom metadata was inserted correctly
- TestUtils.compareMetaDataMaps(channel.getMetaDataColumns(), sourceMap, TestUtils.getCustomMetaData(channel.getChannelId(), sourceMessage.getMessageId(), 0));
- TestUtils.compareMetaDataMaps(channel.getMetaDataColumns(), destinationMap, TestUtils.getCustomMetaData(channel.getChannelId(), sourceMessage.getMessageId(), 1));
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- dao.close();
- channel.undeploy();
- }
- }
+ // Attachment storage and retrieval is covered against every dialect by
+ // ci/tests/230-attachments.
/*
* Create new channel and connector messages Insert content for the messages and assert that: -
@@ -704,263 +604,9 @@ public final void testMarkAsProcessed() throws Exception {
// TODO testResetMessage
- /*
- * Create a new channel Process messages directly through Channel.process, assert that: - Each
- * message was inserted successfully - TEST_SIZE messages were added to the testing list - The
- * channel statistics were updated correctly
- *
- * Iterate through each message in the list, assert that: - Each message, connector message, and
- * message content row was deleted - The channel statistics were not deleted
- *
- * Do the same thing as above, except this time delete the channel statistics as well, assert
- * that: - The channel statistics were deleted
- */
- @Test
- public void testDeleteMessage() throws Exception { // FIXME
- TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- channel.deploy();
- channel.start(null);
-
- DonkeyDao dao = daoFactory.getDao();
-
- try {
- logger.info("Testing DonkeyDao.deleteMessage...");
-
- // Test deleting messages without deleting statistics
- testDeleteMessage(channel, false, dao);
-
- // Test deleting messages with deleting statistics
- testDeleteMessage(channel, true, dao);
- } finally {
- dao.close();
- channel.stop();
- channel.undeploy();
- }
- }
-
- private void testDeleteMessage(TestChannel channel, boolean deleteStatistics, DonkeyDao dao) throws Exception {
- List messages = new ArrayList();
- TestUtils.deleteChannelStatistics(channel.getChannelId());
-
- // Process a bunch of messages through the channel
- for (int i = 1; i <= TEST_SIZE; i++) {
- ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0);
-
- // Bypass the source connector so we can retrieve the Message object
- Message message = channel.process(sourceMessage, true);
-
- // Assert that each message was successfully created
- TestUtils.assertMessageExists(message, true);
- messages.add(message);
- }
-
- // Assert that TEST_SIZE messages were added
- assertEquals(TEST_SIZE, messages.size());
-
- Map> channelStats = ChannelController.getInstance().getStatistics().getChannelStats(channelId);
-
- try {
- // Assert that the statistics were updated, ignore the RECEIVED status for the source/aggregate
- assertNotNull(channelStats);
- assertNotNull(channelStats.get(null));
- assertNotNull(channelStats.get(null).get(Status.SENT));
- assertNotNull(channelStats.get(0));
- assertNotNull(channelStats.get(1));
- assertNotNull(channelStats.get(1).get(Status.RECEIVED));
- assertNotNull(channelStats.get(1).get(Status.SENT));
-
- assertEquals(TEST_SIZE, channelStats.get(null).get(Status.SENT).intValue());
- assertEquals(TEST_SIZE, channelStats.get(1).get(Status.RECEIVED).intValue());
- assertEquals(TEST_SIZE, channelStats.get(1).get(Status.SENT).intValue());
- } catch (AssertionError e) {
- for (Entry> entry : channelStats.entrySet()) {
- System.out.printf("metaDataId %-5s: %s\n", entry.getKey(), entry.getValue());
- }
-
- throw e;
- }
-
- // Delete all the messages that were processed
- for (Message message : messages) {
- if (deleteStatistics) {
- dao.deleteMessageStatistics(message.getChannelId(), message.getMessageId(), null);
- }
-
- dao.deleteMessage(message.getChannelId(), message.getMessageId());
- dao.commit();
-
- // Assert that each message was successfully deleted
- for (ConnectorMessage connectorMessage : message.getConnectorMessages().values()) {
- for (ContentType contentType : ContentType.getMessageTypes()) {
- if (connectorMessage.getMessageContent(contentType) != null) {
- // Assert that each content row was deleted
- TestUtils.assertMessageContentDoesNotExist(connectorMessage.getMessageContent(contentType));
- }
- }
-
- // Assert that each metadata row was deleted
- TestUtils.assertConnectorMessageDoesNotExist(connectorMessage);
- }
-
- // Assert that the message row itself was deleted
- TestUtils.assertMessageDoesNotExist(message);
- }
-
- if (deleteStatistics) {
- // Assert that the statistics were decremented
- channelStats = ChannelController.getInstance().getStatistics().getChannelStats(channelId);
- assertEquals(0, channelStats.get(null).get(Status.RECEIVED).intValue());
- assertEquals(0, channelStats.get(null).get(Status.SENT).intValue());
- assertEquals(0, channelStats.get(0).get(Status.RECEIVED).intValue());
- assertEquals(0, channelStats.get(1).get(Status.RECEIVED).intValue());
- assertEquals(0, channelStats.get(1).get(Status.SENT).intValue());
- } else {
- // Assert that the statistics were not deleted, ignore the RECEIVED status for the source/aggregate
- channelStats = ChannelController.getInstance().getStatistics().getChannelStats(channelId);
- assertEquals(TEST_SIZE, channelStats.get(null).get(Status.SENT).intValue());
- assertEquals(TEST_SIZE, channelStats.get(1).get(Status.RECEIVED).intValue());
- assertEquals(TEST_SIZE, channelStats.get(1).get(Status.SENT).intValue());
- }
- }
-
- /*
- * Deploy a new channel, process messages For each message, assert that: - Each connector
- * message and content was inserted
- *
- * Then delete the connector messages and assert: - Each connector message and content was
- * deleted
- */
- @Test
- public final void testDeleteConnectorMessages() throws Exception {
- TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- channel.deploy();
- channel.start(null);
-
- try {
- logger.info("Testing DonkeyDao.deleteConnectorMessages...");
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- Message message = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory);
- channel.process(message.getConnectorMessages().get(0), true);
-
- for (ConnectorMessage connectorMessage : message.getConnectorMessages().values()) {
- TestUtils.assertConnectorMessageExists(connectorMessage, true);
- }
-
- DonkeyDao dao = null;
-
- try {
- dao = daoFactory.getDao();
- dao.deleteConnectorMessages(channel.getChannelId(), message.getMessageId(), message.getConnectorMessages().keySet());
- dao.commit();
- } finally {
- TestUtils.close(dao);
- }
-
- for (ConnectorMessage connectorMessage : message.getConnectorMessages().values()) {
- for (ContentType contentType : ContentType.getMessageTypes()) {
- MessageContent messageContent = connectorMessage.getMessageContent(contentType);
- if (messageContent != null) {
- TestUtils.assertMessageContentDoesNotExist(messageContent);
- }
- }
- TestUtils.assertConnectorMessageDoesNotExist(connectorMessage);
- }
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- channel.stop();
- channel.undeploy();
- }
- }
-
- // TODO testDeleteMessageContent
-
- // TODO testDeleteMessageAttachments
-
- // TODO testDeleteMessageStatistics
-
- /*
- * Deploy a new channel, process messages Delete all messages for the channel and assert: - The
- * message table was truncated - The message metadata table was truncated - The message content
- * table was truncated - The message custom metadata table was truncated - The message
- * attachment table was truncated
- */
- @Test
- public final void testDeleteAllMessages() throws Exception {
- TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- channel.deploy();
- channel.start(null);
-
- logger.info("Testing DonkeyDao.deleteAllMessages...");
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
- }
-
- DonkeyDao dao = null;
-
- try {
- dao = daoFactory.getDao();
- dao.deleteAllMessages(channel.getChannelId());
- dao.commit();
- } finally {
- TestUtils.close(dao);
- }
-
- Connection connection = null;
- PreparedStatement statement = null;
- ResultSet result = null;
-
- try {
- // Assert that all the message tables have been truncated
- long localChannelId = ChannelController.getInstance().getLocalChannelId(channel.getChannelId());
- connection = TestUtils.getConnection();
-
- statement = connection.prepareStatement("SELECT * FROM d_m" + localChannelId);
- result = statement.executeQuery();
- assertFalse(result.next());
- result.close();
- statement.close();
-
- statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId);
- result = statement.executeQuery();
- assertFalse(result.next());
- result.close();
- statement.close();
-
- statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId);
- result = statement.executeQuery();
- assertFalse(result.next());
- result.close();
- statement.close();
-
- statement = connection.prepareStatement("SELECT * FROM d_mcm" + localChannelId);
- result = statement.executeQuery();
- assertFalse(result.next());
- result.close();
- statement.close();
-
- statement = connection.prepareStatement("SELECT * FROM d_ma" + localChannelId);
- result = statement.executeQuery();
- assertFalse(result.next());
- result.close();
- statement.close();
-
- System.out.println(daoTimer.getLog());
- } finally {
- TestUtils.close(result);
- TestUtils.close(statement);
- TestUtils.close(connection);
-
- channel.stop();
- channel.undeploy();
- }
- }
+ // Deleting one message, one connector message, or every message in a channel - and the
+ // cascade into content, attachments, custom metadata and statistics that each one has to
+ // carry out - is covered against every dialect by the 240-message-deletion smoke tests.
/*
* Use createChannel to create some new channels; assert: - The channel ID and local channel ID
@@ -1127,102 +773,6 @@ public final void testRemoveChannel() throws Exception {
System.out.println(daoTimer.getLog());
}
- /*
- * Deploy a new channel, manually add metadata columns using addMetaDataColumn, and assert that:
- * - All the columns were successfully added
- */
- @Test
- public final void testAddMetaDataColumn() throws Exception {
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- List metaDataColumns = new ArrayList();
-
- try {
- logger.info("Testing DonkeyDao.addMetaDataColumn...");
-
- channel.deploy();
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- DonkeyDao dao = null;
-
- try {
- dao = daoFactory.getDao();
-
- for (MetaDataColumnType type : MetaDataColumnType.values()) {
- MetaDataColumn metaDataColumn = new MetaDataColumn(type.toString() + "column" + i, type, null);
- dao.addMetaDataColumn(channel.getChannelId(), metaDataColumn);
- metaDataColumns.add(metaDataColumn);
- }
-
- logger.debug("Adding metadata column set " + i);
- dao.commit();
- } finally {
- TestUtils.close(dao);
- }
-
- // Assert that the columns were added
- assertEquals(metaDataColumns, TestUtils.getExistingMetaDataColumns(channel.getChannelId()));
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- channel.undeploy();
- }
- }
-
- /*
- * Deploy a new channel, add metadata columns, then use removeMetaDataColumn to delete all the
- * columns added Get the list of existing metadata columns in the database, and assert: - All
- * the columns previously added were successfully removed
- */
- @Test
- public final void testRemoveMetaDataColumn() throws Exception {
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- List metaDataColumns = new ArrayList();
-
- try {
- logger.info("Testing DonkeyDao.addMetaDataColumn...");
-
- channel.deploy();
- DonkeyDao dao = null;
-
- try {
- dao = daoFactory.getDao();
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- for (MetaDataColumnType type : MetaDataColumnType.values()) {
- MetaDataColumn metaDataColumn = new MetaDataColumn(type.toString() + "column" + i, type, null);
- dao.addMetaDataColumn(channel.getChannelId(), metaDataColumn);
- metaDataColumns.add(metaDataColumn);
- }
- }
-
- dao.commit();
-
- // Remove the columns
- for (MetaDataColumn metaDataColumn : metaDataColumns) {
- dao.removeMetaDataColumn(channel.getChannelId(), metaDataColumn.getName());
- }
-
- dao.commit();
- } finally {
- TestUtils.close(dao);
- }
-
- List databaseMetaDataColumns = TestUtils.getExistingMetaDataColumns(channel.getChannelId());
-
- // Assert that the columns in the database do not contain any of the columns previously added
- for (MetaDataColumn metaDataColumn : metaDataColumns) {
- assertFalse(databaseMetaDataColumns.contains(metaDataColumn));
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- channel.undeploy();
- }
- }
-
// TODO testResetStatistics
// TODO testResetAllStatistics
@@ -1761,92 +1311,10 @@ public final void testGetUnfinishedMessages() throws Exception {
}
}
- /*
- * Create a list of metadata columns and add the list to the channel's metadata columns Deploy
- * the channel, and assert that: - The list of metadata columns matches the one returned by
- * getMetaDataColumns
- */
- @Test
- public final void testGetMetaDataColumns() throws Exception {
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
-
- List metaDataColumns = new ArrayList();
- for (MetaDataColumnType type : MetaDataColumnType.values()) {
- metaDataColumns.add(new MetaDataColumn(type.toString() + "column", type, null));
- }
- channel.setMetaDataColumns(metaDataColumns);
-
- try {
- logger.info("Testing DonkeyDao.getMetaDataColumns...");
-
- channel.deploy();
-
- List daoMetaDataColumns;
- DonkeyDao dao = null;
-
- try {
- dao = daoFactory.getDao();
- daoMetaDataColumns = dao.getMetaDataColumns(channel.getChannelId());
- } finally {
- TestUtils.close(dao);
- }
-
- assertEquals(metaDataColumns.size(), daoMetaDataColumns.size());
-
- for (MetaDataColumn column : daoMetaDataColumns) {
- assertTrue(column.getName(), metaDataColumns.contains(column));
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- channel.undeploy();
- }
- }
-
// TODO testGetMessageAttachment
- /*
- * Start up a new channel, assert that: - The channel statistics in the database are the same as
- * the ones returned from getChannelStatistics
- *
- * Then send messages, and after each one assert: - The channel statistics in the database are
- * the same as the ones returned from getChannelStatistics
- */
- @Test
- public final void testGetChannelStatistics() throws Exception {
- // TODO also test getChannelTotalStatistics here
-
- Channel channel = TestUtils.createDefaultChannel(channelId, serverId);
- channel.deploy();
- channel.start(null);
-
- DispatchResult dispatchResult = null;
-
- try {
- dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(TestUtils.TEST_HL7_MESSAGE));
- } finally {
- channel.getSourceConnector().finishDispatch(dispatchResult);
- }
-
- try {
- logger.info("Testing DonkeyDao.getChannelStatistics...");
-
- // Assert that the statistics are correct
- assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channel.getChannelId()));
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
-
- // Assert that the statistics are correct
- assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channel.getChannelId()));
- }
-
- System.out.println(daoTimer.getLog());
- } finally {
- channel.stop();
- channel.undeploy();
- }
- }
+ // Reading a channel's statistics back - and the per-dialect statements that wrote them
+ // there - is covered against every dialect by the 250-statistics smoke tests.
/**
* Sends messages through 5 channels with (maxConnections * 2) asynchronous destinations for 10
@@ -1916,4 +1384,11 @@ public final void testJdbcDaoStatementCache() throws Exception {
// channel.undeploy();
// }
// }
+ /*
+ * Replaced by the ci/tests/200-custom-metadata-columns fixtures, which deploy a channel with a
+ * column of each type and assert the values the engine stored and read back, and by
+ * CustomMetaDataColumnRedeployTest, which edits a deployed channel's column list to reach
+ * removeMetaDataColumn. Covered there: testInsertMetaData, testAddMetaDataColumn,
+ * testRemoveMetaDataColumn and testGetMetaDataColumns.
+ */
}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/FilterTransformerTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/FilterTransformerTests.java
deleted file mode 100644
index fe9ad10219..0000000000
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/FilterTransformerTests.java
+++ /dev/null
@@ -1,305 +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.donkey.test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import java.util.Calendar;
-import java.util.Map;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import com.mirth.connect.donkey.model.DonkeyException;
-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.MessageSerializer;
-import com.mirth.connect.donkey.model.message.MessageSerializerException;
-import com.mirth.connect.donkey.model.message.Status;
-import com.mirth.connect.donkey.server.Donkey;
-import com.mirth.connect.donkey.server.StartException;
-import com.mirth.connect.donkey.server.channel.FilterTransformerExecutor;
-import com.mirth.connect.donkey.server.channel.FilterTransformerResult;
-import com.mirth.connect.donkey.server.channel.components.FilterTransformerException;
-import com.mirth.connect.donkey.server.message.DataType;
-import com.mirth.connect.donkey.test.util.TestAutoResponder;
-import com.mirth.connect.donkey.test.util.TestDataType;
-import com.mirth.connect.donkey.test.util.TestFilterTransformer;
-import com.mirth.connect.donkey.test.util.TestSerializer;
-import com.mirth.connect.donkey.test.util.TestUtils;
-
-public class FilterTransformerTests {
- private static int TEST_SIZE = 10;
- private static String channelId = TestUtils.DEFAULT_CHANNEL_ID;
- private static String channelName = TestUtils.DEFAULT_CHANNEL_ID;
- private static String serverId = TestUtils.DEFAULT_SERVER_ID;
- private static String testMessage = TestUtils.TEST_HL7_MESSAGE;
-
- private Logger logger = LogManager.getLogger(this.getClass());
-
- @BeforeClass
- final public static void beforeClass() throws StartException {
- Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration());
- }
-
- @AfterClass
- final public static void afterClass() throws StartException {
- Donkey.getInstance().stopEngine();
- }
-
- /*
- * For each test, create a new connector message with some raw content
- *
- * Set the inbound data type to a FailingTestDataType which always throws a SerializerException
- * Process the connector message, and assert that: - The status is ERROR - The transformed
- * content is null - The encoded content is null
- *
- * Set the filter/transformer interface to one that always returns false Process the connector
- * message, and assert that: - The status is FILTERED - The transformed content is not null -
- * The encoded content is null
- *
- * Set the filter/transformer interface to one that always throws a FilterTransformerException
- * Process the connector message, and assert that: - The status is ERROR - The transformed
- * content is not null - The encoded content is null
- *
- * Set the processed raw content on the connector message Process the connector message, and
- * assert that: - The status is TRANSFORMED - The transformed content is not null - The encoded
- * content is not null - The transformed content is equal to the serialized processed raw
- * content - The encoded content is equal to the serialized/deserialized processed raw content
- */
- @Test
- public void testWithFilterTransformer() throws Exception {
- ConnectorMessage connectorMessage;
- FilterTransformerExecutor filterTransformerExecutor;
-
- class FailingTestSerializer implements MessageSerializer {
- @Override
- public boolean isSerializationRequired(boolean isXml) {
- return false;
- }
-
- @Override
- public String transformWithoutSerializing(String message, MessageSerializer outboundSerializer) {
- return message;
- }
-
- @Override
- public String toXML(String message) throws MessageSerializerException {
- throw new MessageSerializerException("Inbound serialization failed.");
- }
-
- @Override
- public String fromXML(String message) throws MessageSerializerException {
- throw new MessageSerializerException("Outbound serialization failed.");
- }
-
- @Override
- public void populateMetaData(String message, Map map) {}
-
- @Override
- public String toJSON(String message) throws MessageSerializerException {
- return null;
- }
-
- @Override
- public String fromJSON(String message) throws MessageSerializerException {
- return null;
- }
- }
-
- class FailingTestDataType extends DataType {
- public FailingTestDataType() {
- super("HL7V2", new FailingTestSerializer(), new TestAutoResponder());
- }
- }
-
- logger.info("Testing FilterTransformerExecutor.processConnectorMessage with a FilterTransformer...");
-
- /*
- * Assert that if inbound serialization failed, the status is set to ERROR, and the
- * transformed/encoded content is not set
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new FailingTestDataType(), new TestDataType());
- filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer());
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false));
-
- try {
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
- } catch (MessageSerializerException e) {
- }
-
- assertNull(connectorMessage.getTransformed());
- assertNull(connectorMessage.getEncoded());
- }
-
- /*
- * Assert that if the message is filtered, the status is set to FILTERED, the transformed
- * content is set, and the encoded content is not set
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType());
- filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer() {
- @Override
- public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException {
- return new FilterTransformerResult(true, null);
- }
- });
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false));
-
- try {
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
- } catch (FilterTransformerException e) {
- }
-
- assertEquals(Status.FILTERED, connectorMessage.getStatus());
- assertNotNull(connectorMessage.getTransformed());
- assertNull(connectorMessage.getEncoded());
- }
-
- /*
- * Assert that if the filter/transformer interface throws an exception, then the status is
- * set to ERROR, the transformed content is set, and the encoded content is not set
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType());
- filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer() {
- @Override
- public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException {
- throw new FilterTransformerException("Failed to run filter/transformer.", new Exception(), null);
- }
- });
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false));
-
- try {
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
- } catch (FilterTransformerException e) {
- }
-
- assertNotNull(connectorMessage.getTransformed());
- assertNull(connectorMessage.getEncoded());
- }
-
- /*
- * Assert that if the outbound deserialization fails, then the status is set to ERROR, the
- * transformed content is set, and the encoded content is not set
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new FailingTestDataType());
- filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer());
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false));
-
- try {
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
- } catch (DonkeyException e) {
- }
-
- assertNotNull(connectorMessage.getTransformed());
- assertNull(connectorMessage.getEncoded());
- }
-
- /*
- * Assert that if everything runs without errors, then the status is set to TRANSFORMED, and
- * the transformed/encoded content is set
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType());
- filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer());
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false));
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
-
- assertEquals(Status.TRANSFORMED, connectorMessage.getStatus());
- assertNotNull(connectorMessage.getTransformed());
- assertNotNull(connectorMessage.getEncoded());
- }
-
- /*
- * Assert that if the processed raw content is set, then the status is set to TRANSFORMED,
- * the transformed/encoded content is set, and the transformed content is the serialized
- * processed raw content rather than the raw content
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType());
- filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer());
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, "", "HL7V2", false));
- connectorMessage.setProcessedRaw(new MessageContent(channelId, 1, 1, ContentType.PROCESSED_RAW, testMessage, "HL7V2", false));
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
-
- assertEquals(Status.TRANSFORMED, connectorMessage.getStatus());
- assertNotNull(connectorMessage.getTransformed());
- assertNotNull(connectorMessage.getEncoded());
- assertEquals((new TestSerializer()).toXML(testMessage), connectorMessage.getTransformed().getContent());
- assertEquals((new TestSerializer()).fromXML((new TestSerializer()).toXML(testMessage)), connectorMessage.getEncoded().getContent());
- }
- }
-
- /*
- * For each test, create a new connector message with some raw content
- *
- * Process the connector message, and assert that: - The status is TRANSFORMED - The transformed
- * content is null - The encoded content is equal to the raw content
- *
- * Set the processed raw content on the connector message Process the connector message, and
- * assert that: - The status is TRANSFORMED - The transformed content is null - The encoded
- * content is equal to the processed raw content
- */
- @Test
- public void testWithoutFilterTransformer() throws Exception {
- ConnectorMessage connectorMessage;
- FilterTransformerExecutor filterTransformerExecutor;
-
- logger.info("Testing FilterTransformerExecutor.processConnectorMessage without a FilterTransformer...");
-
- /*
- * Assert that if the processed raw content is not set, then the status is set to
- * TRANSFORMED, the transformed content is not set, and the encoded content is set to the
- * raw content
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType());
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false));
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
-
- assertEquals(Status.TRANSFORMED, connectorMessage.getStatus());
- assertNull(connectorMessage.getTransformed());
- assertEquals(testMessage, connectorMessage.getEncoded().getContent());
- }
-
- /*
- * Assert that if the processed raw content is set, then the status is set to TRANSFORMED,
- * the transformed content is not set, the encoded content is set to the processed raw
- * content rather than the raw content
- */
- filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType());
- for (int i = 1; i <= TEST_SIZE; i++) {
- connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED);
- connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, "", "HL7V2", false));
- connectorMessage.setProcessedRaw(new MessageContent(channelId, 1, 1, ContentType.PROCESSED_RAW, testMessage, "HL7V2", false));
- filterTransformerExecutor.processConnectorMessage(connectorMessage);
-
- assertEquals(Status.TRANSFORMED, connectorMessage.getStatus());
- assertNull(connectorMessage.getTransformed());
- assertEquals(testMessage, connectorMessage.getEncoded().getContent());
- }
- }
-}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java
index e9dce58d9e..b4f3d60ef0 100644
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java
+++ b/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java
@@ -14,10 +14,8 @@
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
-import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
-import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -27,20 +25,12 @@
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.Message;
-import com.mirth.connect.donkey.model.message.MessageContent;
import com.mirth.connect.donkey.model.message.RawMessage;
-import com.mirth.connect.donkey.model.message.Status;
import com.mirth.connect.donkey.server.Donkey;
import com.mirth.connect.donkey.server.StartException;
import com.mirth.connect.donkey.server.channel.Channel;
import com.mirth.connect.donkey.server.controllers.ChannelController;
-import com.mirth.connect.donkey.server.controllers.MessageController;
-import com.mirth.connect.donkey.server.data.DonkeyDao;
import com.mirth.connect.donkey.server.data.timed.TimedDaoFactory;
-import com.mirth.connect.donkey.server.queue.ConnectorMessageQueueDataSource;
-import com.mirth.connect.donkey.test.util.TestChannel;
import com.mirth.connect.donkey.test.util.TestUtils;
import com.mirth.connect.donkey.util.ActionTimer;
@@ -126,59 +116,6 @@ public void testCreateNewMessage() throws Exception {
}
}
- @Test
- public void testDeleteMessage() throws Exception {
- TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId);
- channel.getSourceConnector().setRespondAfterProcessing(false);
- channel.getSourceQueue().setDataSource(new ConnectorMessageQueueDataSource(channelId, serverId, 0, Status.RECEIVED, false, TestUtils.getDaoFactory()));
- channel.getSourceQueue().updateSize();
-
- Message message = null;
- ConnectorMessage sourceMessage = null;
- DonkeyDao dao = null;
-
- try {
- dao = TestUtils.getDaoFactory().getDao();
-
- message = new Message();
- message.setMessageId(dao.getNextMessageId(channelId));
- message.setChannelId(channelId);
- message.setServerId(serverId);
- message.setReceivedDate(Calendar.getInstance());
-
- sourceMessage = new ConnectorMessage(channelId, channel.getName(), message.getMessageId(), 0, serverId, message.getReceivedDate(), Status.RECEIVED);
- sourceMessage.setRaw(new MessageContent(channelId, message.getMessageId(), 0, ContentType.RAW, testMessage, null, false));
- message.getConnectorMessages().put(0, sourceMessage);
-
- dao.insertMessage(message);
- dao.insertConnectorMessage(sourceMessage, true, true);
- dao.insertMessageContent(sourceMessage.getRaw());
- dao.commit();
- } finally {
- TestUtils.close(dao);
- }
-
- // put the message in the source queue
- channel.queue(sourceMessage);
-
- // assert that the message exists in the database
- TestUtils.assertMessageExists(message, true);
- TestUtils.assertConnectorMessageExists(sourceMessage, true);
-
- // assert that the message exists in the source queue's memory
- assertTrue(channel.getSourceQueue().contains(sourceMessage));
-
- // delete the message
- Map> messages = new HashMap>();
- messages.put(message.getMessageId(), null);
- MessageController.getInstance().deleteMessages(channelId, messages);
- channel.invalidateQueues();
-
- // assert that the message does not exist in the database
- TestUtils.assertMessageDoesNotExist(message);
- TestUtils.assertConnectorMessageDoesNotExist(sourceMessage);
-
- // assert that the message does not exist in the source queue's memory
- assertTrue(!channel.getSourceQueue().contains(sourceMessage));
- }
+ // Deleting a message, and the queue that still holds it noticing, is covered against every
+ // dialect by the 240-message-deletion smoke tests.
}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/QueueTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/QueueTests.java
deleted file mode 100644
index ccc259a182..0000000000
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/QueueTests.java
+++ /dev/null
@@ -1,504 +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.donkey.test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import com.mirth.connect.donkey.model.channel.DeployedState;
-import com.mirth.connect.donkey.model.message.ConnectorMessage;
-import com.mirth.connect.donkey.model.message.Message;
-import com.mirth.connect.donkey.model.message.RawMessage;
-import com.mirth.connect.donkey.model.message.Status;
-import com.mirth.connect.donkey.server.Donkey;
-import com.mirth.connect.donkey.server.StartException;
-import com.mirth.connect.donkey.server.channel.ChannelException;
-import com.mirth.connect.donkey.server.channel.DestinationChainProvider;
-import com.mirth.connect.donkey.server.channel.DispatchResult;
-import com.mirth.connect.donkey.server.controllers.ChannelController;
-import com.mirth.connect.donkey.server.data.DonkeyDaoFactory;
-import com.mirth.connect.donkey.server.queue.ConnectorMessageQueue;
-import com.mirth.connect.donkey.server.queue.ConnectorMessageQueueDataSource;
-import com.mirth.connect.donkey.server.queue.SourceQueue;
-import com.mirth.connect.donkey.test.util.TestChannel;
-import com.mirth.connect.donkey.test.util.TestDispatcher;
-import com.mirth.connect.donkey.test.util.TestDispatcherProperties;
-import com.mirth.connect.donkey.test.util.TestPostProcessor;
-import com.mirth.connect.donkey.test.util.TestPreProcessor;
-import com.mirth.connect.donkey.test.util.TestSourceConnector;
-import com.mirth.connect.donkey.test.util.TestUtils;
-
-public class QueueTests {
- private static int TEST_SIZE = 10;
- private static String channelId = TestUtils.DEFAULT_CHANNEL_ID;
- private static String channelName = TestUtils.DEFAULT_CHANNEL_ID;
- private static String serverId = TestUtils.DEFAULT_SERVER_ID;
- private static String testMessage = TestUtils.TEST_HL7_MESSAGE;
- private static DonkeyDaoFactory daoFactory;
-
- @BeforeClass
- final public static void beforeClass() throws StartException {
- Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration());
- daoFactory = TestUtils.getDaoFactory();
- }
-
- @AfterClass
- final public static void afterClass() throws StartException {
- Donkey.getInstance().stopEngine();
- }
-
- /*
- * Create a new DatabaseLinkedBlockingQueue Set the select and count statements, fill the queue
- * buffer, and assert that: - The buffer capacity was set correctly - The buffer size is correct
- *
- * Send messages, then assert that: - All messages were successfully put in the queue - The
- * buffer size is still correct
- *
- * Flush the queue, then assert that: - The queue size shrank correctly - The buffer size shrank
- * correctly
- */
- @Test
- public final void testDatabaseLinkedBlockingQueue() throws SQLException {
- int testSize = TEST_SIZE;
- int bufferCapacity = 10;
-
- TestUtils.initChannel(channelId);
-
- SourceQueue queue = new SourceQueue();
- queue.setBufferCapacity(bufferCapacity);
- queue.setDataSource(new ConnectorMessageQueueDataSource(channelId, serverId, 0, Status.RECEIVED, false, daoFactory));
- queue.updateSize();
- queue.fillBuffer();
-
- assertEquals(bufferCapacity, queue.getBufferCapacity());
- assertEquals(Math.min(bufferCapacity, queue.size()), queue.getBufferSize());
-
- int initialSize = queue.size();
- int initialBufferSize = queue.getBufferSize();
-
- for (int i = 0; i < testSize; i++) {
- ConnectorMessage connectorMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channelId, channelName, serverId, daoFactory).getConnectorMessages().get(0);
- queue.add(connectorMessage);
- }
-
- assertEquals(initialSize + testSize, queue.size());
- assertEquals(Math.min(bufferCapacity, queue.size()), queue.getBufferSize());
-
- for (int i = 0; i < testSize; i++) {
- try {
- queue.poll(10, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- assertEquals(initialSize, queue.size());
- assertEquals(Math.max(0, initialBufferSize - testSize), queue.getBufferSize());
- }
-
- /*
- * Start up a new channel, and assert that: - The channel's queue thread is currently running
- * Then stop the channel and assert that: - The channel's queue thread has stopped running
- *
- * While the channel is stopped, send messages, assert that: - The queue size is equal to the
- * test size Start the channel back up, wait a while, assert that: - The queue size is zero
- *
- * While the channel is still running, send messages, assert that: - The queue size is greater
- * than zero Wait a while, then assert that: - The queue size is zero
- */
- @Test
- public final void testSourceQueue() throws Exception {
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
- channel.getSourceConnector().setRespondAfterProcessing(false);
-
- // Start up a default channel
- channel.deploy();
- channel.start(null);
-
- // give the queue thread time to start
- Thread.sleep(3000);
-
- // Assert that the source queue thread is running
- assertTrue(channel.isQueueThreadRunning());
-
- // Stop the channel
- channel.stop();
-
- // Assert that the source queue thread is not longer running
- assertFalse(channel.isQueueThreadRunning());
-
- // Place messages in the channel's source queue while the channel is stopped
- for (int i = 1; i <= TEST_SIZE; i++) {
- ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0);
- channel.queue(sourceMessage);
- }
-
- // Assert that all the messages were successfully placed in the queue
- assertEquals(channel.getSourceQueue().size(), TEST_SIZE);
-
- // Start up the channel
- channel.start(null);
-
- Thread.sleep(500 * TEST_SIZE);
-
- // Assert that the queue has been cleared
- assertEquals(channel.getSourceQueue().size(), 0);
-
- // Place messages in the channel's source queue while the channel is started
- for (int i = 1; i <= TEST_SIZE; i++) {
- ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0);
- channel.queue(sourceMessage);
- }
-
- // Assert that the queue size is greater than zero (may fail for small test sizes)
- assertTrue(channel.getSourceQueue().size() > 0);
-
- Thread.sleep(500 * TEST_SIZE);
-
- // Assert that the queue has cleared
- assertEquals(channel.getSourceQueue().size(), 0);
-
- channel.stop();
- channel.undeploy();
- ChannelController.getInstance().removeChannel(channelId);
- }
-
- /*
- * Start up a new channel, queue up more messages in the channel than the size of the source
- * queue buffer
- *
- * Send a message (not waiting for destinations) that will cause the data to get written to the
- * database, but the queue size is never incremented
- *
- * Asynchronously queue up another message normally, wait a bit to let the asynchronous message
- * get written to the database and queued
- *
- * Queue up the message from before that wasn't queued, wait for the queue to clear, then assert
- * that: - The number of messages processed through the channel is less than the initial number
- * of messages sent plus two - The second-to-last message in the messageIds list was processed,
- * but the last one was not
- */
- @Test
- public final void testSourceQueueOrderAsync() throws Exception {
- int testSize = 2;
- final List messageIds = new ArrayList();
- final TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
- channel.getSourceConnector().setRespondAfterProcessing(false);
-
- // Deploy a default channel
- channel.deploy();
- channel.start(null);
-
- ConnectorMessageQueue sourceQueue = channel.getSourceQueue();
- int initialSize = sourceQueue.getBufferCapacity() * testSize + 1;
-
- // Queue up messages normally
- System.out.println("Queuing " + initialSize + " messages normally...");
- for (int i = 1; i <= initialSize; i++) {
- DispatchResult dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null));
- messageIds.add(dispatchResult.getMessageId());
- channel.getSourceConnector().finishDispatch(dispatchResult);
- }
-
- ConnectorMessage sourceMessage = null;
-
- /*
- * Send a message (not waiting for destinations) that will cause the data to get written to
- * the database, but the queue size is never incremented
- */
- System.out.println("Saving a message to the database without calling the queue method...");
- RawMessage rawMessage = new RawMessage(testMessage, null, null);
- sourceMessage = TestUtils.createAndStoreNewMessage(rawMessage, channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0);
- messageIds.add(sourceMessage.getMessageId());
-
- // Asynchronously queue up another message normally
- Thread thread = new Thread() {
- @Override
- public void run() {
- System.out.println("Queuing another message normally and asynchronously...");
- DispatchResult dispatchResult = null;
-
- try {
- dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null));
- } catch (ChannelException e) {
- throw new AssertionError(e);
- } finally {
- channel.getSourceConnector().finishDispatch(dispatchResult);
- }
-
- messageIds.add(dispatchResult.getMessageId());
- }
- };
- thread.start();
-
- /*
- * Wait until the queue has cleared to simulate a delay between committing the message to
- * the database and adding it to the channel's queue. Meanwhile the asynchronous message
- * should be queued
- */
- while (sourceQueue.size() > 0 && channel.isQueueThreadRunning() && channel.getCurrentState() == DeployedState.STARTED) {
- System.out.println("Waiting for queue to clear, size: " + sourceQueue.size());
- Thread.sleep(1000);
- }
-
- /*
- * Queue up the message from before that wasn't queued. Even though this message has the
- * lower message ID, because the asynchronous message was queued before this one (increasing
- * the queue size), the queue buffer will have been filled with THIS message (not the
- * asynchronous one). Therefore, this message should have already been processed through the
- * channel. Queuing the same message again should cause a foreign key constraint violation.
- */
- System.out.println("Calling the queue method for the previous message that wasn't queued...");
- channel.queue(sourceMessage);
-
- // Wait until the queue has cleared
- while (sourceQueue.size() > 0 && channel.isQueueThreadRunning() && channel.getCurrentState() == DeployedState.STARTED) {
- System.out.println("Waiting for queue to clear, size: " + sourceQueue.size());
- Thread.sleep(5000);
- }
-
- // Assert that the number of messages processed through the channel is NOT correct
- assertTrue(channel.getNumMessages() < initialSize + 2);
-
- // Assert that the second-to-last message in the messageIds list was processed, but the last one was not
- assertTrue(channel.getMessageIds().contains(messageIds.get(messageIds.size() - 2)));
- assertFalse(channel.getMessageIds().contains(messageIds.get(messageIds.size() - 1)));
-
- channel.stop();
- channel.undeploy();
-
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Start up a new channel, queue up more messages in the channel than the size of the source
- * queue buffer
- *
- * Synchronize on the channel's source queue so that commits and queue additions happen in the
- * same thread always
- *
- * Send a message (not waiting for destinations) that will cause the data to get written to the
- * database, but the queue size is never incremented
- *
- * Asynchronously queue up another message normally, wait a while to simulate a delay between
- * the commit and queue addition (the asynchronous message should not be queued)
- *
- * Queue up the message from before that wasn't queued, wait for the queue to clear, then assert
- * that: - The number of messages processed through the channel is equal to the initial number
- * of messages sent plus two - The messageIds list is identical (size and order) to the message
- * ID list generated by the test channel (that is, all messages successfully processed through
- * the channel and in the right order)
- */
- @Test
- public final void testSourceQueueOrderSync() throws Exception {
- int testSize = 2;
- final List messageIds = new ArrayList();
- final TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
- channel.getSourceConnector().setRespondAfterProcessing(false);
-
- // Deploy a default channel
- channel.deploy();
- channel.start(null);
-
- ConnectorMessageQueue sourceQueue = channel.getSourceQueue();
- int initialSize = sourceQueue.getBufferCapacity() * testSize + 1;
-
- // Queue up messages normally
- System.out.println("Queuing " + initialSize + " messages normally...");
- for (int i = 1; i <= initialSize; i++) {
- DispatchResult dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null));
- messageIds.add(dispatchResult.getMessageId());
- channel.getSourceConnector().finishDispatch(dispatchResult);
- }
-
- ConnectorMessage sourceMessage = null;
-
- synchronized (channel.getSourceQueue()) {
- /*
- * Send a message (not waiting for destinations) that will cause the data to get written
- * to the database, but the queue size is never incremented
- */
- System.out.println("Saving a message to the database without calling the queue method...");
- RawMessage rawMessage = new RawMessage(testMessage, null, null);
- sourceMessage = TestUtils.createAndStoreNewMessage(rawMessage, channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0);
- messageIds.add(sourceMessage.getMessageId());
-
- // Asynchronously queue up another message normally
- Thread thread = new Thread() {
- @Override
- public void run() {
- System.out.println("Queuing another message normally and asynchronously...");
- DispatchResult dispatchResult = null;
-
- try {
- dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null));
- } catch (ChannelException e) {
- throw new AssertionError(e);
- } finally {
- channel.getSourceConnector().finishDispatch(dispatchResult);
- }
-
- messageIds.add(dispatchResult.getMessageId());
- }
- };
- thread.start();
-
- /*
- * Wait a while to simulate a delay between committing the message to the database and
- * adding it to the channel's queue. The thread processing the asynchronous message
- * should be waiting on this block to finish, so it should not add the message to the
- * source queue
- */
- System.out.println("Waiting ten seconds...");
- Thread.sleep(10000);
-
- // Queue up the message from before that wasn't queued
- System.out.println("Calling the queue method for the previous message that wasn't queued...");
- channel.queue(sourceMessage);
- }
-
- // Wait until the queue has cleared
- while (sourceQueue.size() > 0 && channel.isQueueThreadRunning() && channel.getCurrentState() == DeployedState.STARTED) {
- System.out.println("Waiting for queue to clear, size: " + sourceQueue.size());
- Thread.sleep(5000);
- }
-
- // Assert that the number of messages processed through the channel is correct
- assertEquals(initialSize + 2, channel.getNumMessages());
- // Assert that all messages were processed through the channel in the order of database insertion
- assertTrue(messageIds.equals(channel.getMessageIds()));
-
- channel.stop();
- channel.undeploy();
-
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Create a new channel with a test dispatcher destination connector The dispatcher initially
- * queues all messages Start up the channel, and assert that: - The destination connector queue
- * thread is running
- *
- * Send messages, and assert that: - The queue size is equal to the test size
- *
- * Change the response status that the dispatcher returns to SENT Wait a bit, then assert that:
- * - The queue size is zero
- *
- * Stop the channel, assert that: - The destination connector queue thread is not running
- *
- * Place messages directly in the destination connector queue, assert that: - All the messages
- * were successfully put in the queue
- *
- * Start the channel, wait a bit, then assert that: - The queue size is zero
- */
- @Test
- public final void testDestinationQueue() throws Exception {
- TestUtils.initChannel(channelId);
-
- TestChannel channel = new TestChannel();
-
- channel.setChannelId(channelId);
- channel.setServerId(serverId);
-
- channel.setPreProcessor(new TestPreProcessor());
- channel.setPostProcessor(new TestPostProcessor());
-
- TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector();
- sourceConnector.setChannelId(channel.getChannelId());
- sourceConnector.setChannel(channel);
- channel.setSourceConnector(sourceConnector);
- channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
-
- // The TestDispatcher send method initially always returns a response of QUEUED
- TestDispatcher destinationConnector = new TestDispatcher();
-
- TestDispatcherProperties connectorProperties = new TestDispatcherProperties();
- connectorProperties.setTemplate(testMessage);
- connectorProperties.getDestinationConnectorProperties().setQueueEnabled(true);
- connectorProperties.getDestinationConnectorProperties().setRegenerateTemplate(true);
-
- TestUtils.initDefaultDestinationConnector(destinationConnector, connectorProperties);
- destinationConnector.setChannelId(channelId);
-
- destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer());
- destinationConnector.setMetaDataColumns(channel.getMetaDataColumns());
- destinationConnector.setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
-
- DestinationChainProvider chain = new DestinationChainProvider();
- chain.setChannelId(channelId);
- chain.addDestination(1, destinationConnector);
- channel.addDestinationChainProvider(chain);
-
- // Start up the channel
- channel.deploy();
- channel.start(null);
-
- Thread.sleep(1000);
-
- // Assert that the destination connector queue thread is running
- assertTrue(destinationConnector.isQueueThreadRunning());
-
- // Send messages while the channel is started
- for (int i = 1; i <= TEST_SIZE; i++) {
- sourceConnector.readTestMessage(testMessage);
- }
-
- // Since the messages should all get queued, assert that the queue size is equal to the test size
- assertEquals(destinationConnector.getQueue().size(), TEST_SIZE);
-
- // Tell the dispatcher to now send a response status of SENT
- destinationConnector.setReturnStatus(Status.SENT);
-
- Thread.sleep(500 * TEST_SIZE);
-
- // Assert that all the queued messages have been sent
- assertEquals(destinationConnector.getQueue().size(), 0);
-
- // Stop the channel
- channel.stop();
-
- // Assert that the destination connector queue thread is not running
- assertFalse(destinationConnector.isQueueThreadRunning());
-
- // Place messages directly into the destination connector's queue
- for (int i = 1; i <= TEST_SIZE; i++) {
- synchronized (destinationConnector.getQueue()) {
- Message message = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channelId, channelName, serverId, daoFactory);
- ConnectorMessage destinationMessage = TestUtils.createAndStoreDestinationConnectorMessage(daoFactory, channelId, channelName, serverId, message.getMessageId(), destinationConnector.getMetaDataId(), testMessage, Status.QUEUED);
- destinationConnector.getQueue().add(destinationMessage);
- }
- }
-
- // Assert that all the messages were successfully placed in the queue
- assertEquals(TEST_SIZE, destinationConnector.getQueue().size() - 1);
-
- // Start the channel back up
- channel.start(null);
-
- Thread.sleep(500 * TEST_SIZE);
-
- // Since the dispatcher should still always be returning a response status of SENT, the queue should be clear again
- assertEquals(0, destinationConnector.getQueue().size());
-
- channel.stop();
- channel.undeploy();
- ChannelController.getInstance().removeChannel(channelId);
- }
-}
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java
index c4c8560167..1d414afe86 100644
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java
+++ b/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java
@@ -21,7 +21,6 @@
import com.mirth.connect.donkey.model.channel.SourceConnectorProperties;
import com.mirth.connect.donkey.model.message.Message;
import com.mirth.connect.donkey.model.message.RawMessage;
-import com.mirth.connect.donkey.model.message.Response;
import com.mirth.connect.donkey.model.message.Status;
import com.mirth.connect.donkey.server.Donkey;
import com.mirth.connect.donkey.server.StartException;
@@ -157,134 +156,7 @@ public final void testHandleRawMessage() throws Exception {
}
/*
- * Deploy a new channel, send messages, assert that: - The message was stored in the database -
- * The MessageResponse returned is not null - The response is null - The source connector
- * response was not stored
- *
- * Call storeMessageResponse using a null MessageResponse assert: - The source connector
- * response was not stored
- *
- * Call storeMessageResponse using the returned MessageResponse, assert: - The source connector
- * response was not stored
- *
- * Modify the MessageResponse, creating/setting a new Response object Call storeMessageResponse
- * using the returned MessageResponse, assert: - The source connector response was stored
- */
- @Test
- public final void testStoreMessageResponse() throws Exception {
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
-
- TestSourceConnector sourceConnector = (TestSourceConnector) channel.getSourceConnector();
- sourceConnector.setRespondAfterProcessing(true);
- channel.getResponseSelector().setRespondFromName(SourceConnectorProperties.RESPONSE_SOURCE_TRANSFORMED);
-
- channel.deploy();
- channel.start(null);
-
- for (int i = 1; i <= TEST_SIZE; i++) {
- RawMessage rawMessage = new RawMessage(testMessage);
- DispatchResult dispatchResult = null;
-
- try {
- dispatchResult = sourceConnector.dispatchRawMessage(rawMessage);
- dispatchResult.setAttemptedResponse(true);
-
- if (dispatchResult.getSelectedResponse() != null) {
- dispatchResult.getSelectedResponse().setMessage("response");
- }
- } finally {
- sourceConnector.finishDispatch(dispatchResult);
- }
-
- if (dispatchResult != null) {
- sourceConnector.getMessageIds().add(dispatchResult.getMessageId());
- }
-
- // Assert that the message was created
- Message message = new Message();
- message.setChannelId(channel.getChannelId());
- message.setMessageId(dispatchResult.getMessageId());
- message.setServerId(channel.getServerId());
- message.setProcessed(true);
- TestUtils.assertMessageExists(message, false);
-
- // Assert that the message response is not null
- assertNotNull(dispatchResult);
-
- // Assert that the response is not null
- assertNotNull(dispatchResult.getSelectedResponse());
-
- // Assert that the source connector response was created
- TestUtils.assertResponseExists(channel.getChannelId(), dispatchResult.getMessageId());
- }
-
- channel.stop();
- channel.undeploy();
-
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Deploys and starts a channel, sets the source respond-from name to
- * RESPONSE_SOURCE_TRANSFORMED, sends messages, and asserts that: - Each response returned is
- * TRANSFORMED - TEST_SIZE messages are processed
- *
- * Then sets the source respond-from name to RESPONSE_DESTINATIONS_COMPLETED, sends messages,
- * and asserts that: - Each response returned is SENT - 2*TEST_SIZE messages are processed
- *
- * Then sets the source respond-from name to the destination name, sends messages, and asserts
- * that: - Each response returned is SENT - 3*TEST_SIZE messages are processed
- *
- * Then sets the source respond-from name to an invalid destination name, sends messages, and
- * asserts that: - Each response returned is null - 4*TEST_SIZE messages are processed
+ * testStoreMessageResponse and testGetResponse are re-implemented as CI fixtures in
+ * ci/tests/190-response-handling.
*/
- @Test
- public final void testGetResponse() throws Exception {
- String destinationName = TestUtils.DEFAULT_DESTINATION_NAME;
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId);
- TestSourceConnector sourceConnector = (TestSourceConnector) channel.getSourceConnector();
- Response response = null;
-
- channel.deploy();
- channel.start(null);
-
- channel.getResponseSelector().setRespondFromName(SourceConnectorProperties.RESPONSE_SOURCE_TRANSFORMED);
-
- for (int i = 0; i < TEST_SIZE; i++) {
- response = sourceConnector.readTestMessage(testMessage).getSelectedResponse();
- assertEquals(Status.TRANSFORMED, response.getStatus());
- }
-
- assertEquals(TEST_SIZE, channel.getNumMessages());
-
- channel.getResponseSelector().setRespondFromName(SourceConnectorProperties.RESPONSE_DESTINATIONS_COMPLETED);
-
- for (int i = 0; i < TEST_SIZE; i++) {
- response = sourceConnector.readTestMessage(testMessage).getSelectedResponse();
- assertEquals(Status.SENT, response.getStatus());
- }
-
- assertEquals(TEST_SIZE * 2, channel.getNumMessages());
-
- channel.getResponseSelector().setRespondFromName("d1");
-
- for (int i = 0; i < TEST_SIZE; i++) {
- response = sourceConnector.readTestMessage(testMessage).getSelectedResponse();
- assertEquals(Status.SENT, response.getStatus());
- }
-
- assertEquals(TEST_SIZE * 3, channel.getNumMessages());
-
- channel.getResponseSelector().setRespondFromName(destinationName + "lolwut");
-
- for (int i = 0; i < TEST_SIZE; i++) {
- response = sourceConnector.readTestMessage(testMessage).getSelectedResponse();
- assertEquals(null, response);
- }
-
- assertEquals(TEST_SIZE * 4, channel.getNumMessages());
-
- channel.stop();
- channel.undeploy();
- }
}
\ No newline at end of file
diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/StatisticsTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/StatisticsTests.java
deleted file mode 100644
index 10a93925d8..0000000000
--- a/donkey/src/test/java/com/mirth/connect/donkey/test/StatisticsTests.java
+++ /dev/null
@@ -1,569 +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.donkey.test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import com.mirth.connect.donkey.model.DonkeyException;
-import com.mirth.connect.donkey.model.channel.ConnectorProperties;
-import com.mirth.connect.donkey.model.message.ConnectorMessage;
-import com.mirth.connect.donkey.model.message.Response;
-import com.mirth.connect.donkey.model.message.Status;
-import com.mirth.connect.donkey.server.Donkey;
-import com.mirth.connect.donkey.server.StartException;
-import com.mirth.connect.donkey.server.channel.DestinationChainProvider;
-import com.mirth.connect.donkey.server.channel.DestinationConnector;
-import com.mirth.connect.donkey.server.channel.FilterTransformerExecutor;
-import com.mirth.connect.donkey.server.channel.FilterTransformerResult;
-import com.mirth.connect.donkey.server.channel.components.FilterTransformerException;
-import com.mirth.connect.donkey.server.controllers.ChannelController;
-import com.mirth.connect.donkey.server.data.DonkeyDaoFactory;
-import com.mirth.connect.donkey.server.queue.ConnectorMessageQueueDataSource;
-import com.mirth.connect.donkey.server.queue.DestinationQueue;
-import com.mirth.connect.donkey.test.util.TestChannel;
-import com.mirth.connect.donkey.test.util.TestDataType;
-import com.mirth.connect.donkey.test.util.TestDispatcher;
-import com.mirth.connect.donkey.test.util.TestDispatcherProperties;
-import com.mirth.connect.donkey.test.util.TestFilterTransformer;
-import com.mirth.connect.donkey.test.util.TestPostProcessor;
-import com.mirth.connect.donkey.test.util.TestPreProcessor;
-import com.mirth.connect.donkey.test.util.TestResponseTransformer;
-import com.mirth.connect.donkey.test.util.TestSourceConnector;
-import com.mirth.connect.donkey.test.util.TestUtils;
-
-public class StatisticsTests {
- private static long TEST_SIZE = 100;
- private static String channelId = TestUtils.DEFAULT_CHANNEL_ID;
- private static String serverId = TestUtils.DEFAULT_SERVER_ID;
- private static String testMessage = TestUtils.TEST_HL7_MESSAGE;
- private static DonkeyDaoFactory daoFactory;
- private Logger logger = LogManager.getLogger(this.getClass());
-
- @BeforeClass
- final public static void beforeClass() throws StartException {
- Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration());
- daoFactory = TestUtils.getDaoFactory();
- }
-
- @AfterClass
- final public static void afterClass() throws StartException {
- Donkey.getInstance().stopEngine();
- }
-
- /*
- * Create a "normal" channel with four destination connectors where all messages process through
- * the source connector with a final status of RECEIVED, and process through each destination
- * connector with a status of SENT
- *
- * Send messages, assert that: - The source connector stats have TEST_SIZE for RECEIVED and
- * TRANSFORMED - The destination connector stats have TEST_SIZE for RECEIVED and SENT - The
- * aggregate stats are correct (RECEIVED/TRANSFORMED is the same as the source connector,
- * PENDING/SENT/QUEUED is the same as the destination connectors combined, FILTERED/ERROR is the
- * same as all connectors combined)
- */
- @Test
- public final void testStatistics1() throws Exception {
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId, true, 2, 2);
-
- channel.deploy();
- channel.start(null);
-
- // Send messages through the channel
- for (int i = 1; i <= TEST_SIZE; i++) {
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
- }
-
- // Assert that the messages were sent
- assertEquals(TEST_SIZE, channel.getNumMessages());
-
- // Assert the source connector stats are correct
-
- statsEqual(channel.getChannelId(), 0, TEST_SIZE, 0l, TEST_SIZE, 0l, 0l, 0l);
- // Assert that destination connector stats are correct
- statsEqual(channel.getChannelId(), 1, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l);
- statsEqual(channel.getChannelId(), 2, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l);
- statsEqual(channel.getChannelId(), 3, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l);
- statsEqual(channel.getChannelId(), 4, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l);
- // Assert the aggregate stats are correct
- assertTrue(channelStatsCorrect());
-
- channel.stop();
- channel.undeploy();
-
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Create a channel where all messages are filtered on the source connector
- *
- * Send messages, assert that: - The source connector stats have TEST_SIZE for RECEIVED and
- * FILTERED - The destination connector stats are all zeroes - The aggregate stats are correct
- * (RECEIVED/TRANSFORMED is the same as the source connector, PENDING/SENT/QUEUED is the same as
- * the destination connectors combined, FILTERED/ERROR is the same as all connectors combined)
- */
- @Test
- public final void testStatistics2() throws Exception {
- TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId, true, 2, 2);
-
- TestFilterTransformer filterTransformer = new TestFilterTransformer() {
- @Override
- public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException {
- super.doFilterTransform(message);
- return new FilterTransformerResult(true, null);
- }
- };
- channel.getSourceConnector().getFilterTransformerExecutor().setFilterTransformer(filterTransformer);
-
- channel.deploy();
- channel.start(null);
-
- // Send messages through the channel
- for (int i = 1; i <= TEST_SIZE; i++) {
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
- }
-
- // Assert that the messages were sent
- assertEquals(TEST_SIZE, channel.getNumMessages());
-
- // Assert the source connector stats are correct
- statsEqual(channel.getChannelId(), 0, TEST_SIZE, TEST_SIZE, 0L, 0L, 0L, 0L);
- // Assert that destination connector stats are correct
- statsEqual(channel.getChannelId(), 1, 0L, 0L, 0L, 0L, 0L, 0L);
- statsEqual(channel.getChannelId(), 2, 0L, 0L, 0L, 0L, 0L, 0L);
- statsEqual(channel.getChannelId(), 3, 0L, 0L, 0L, 0L, 0L, 0L);
- statsEqual(channel.getChannelId(), 4, 0L, 0L, 0L, 0L, 0L, 0L);
- // Assert the aggregate stats are correct
- assertTrue(channelStatsCorrect());
-
- channel.stop();
- channel.undeploy();
-
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Create a channel where each destination results in a different status (FILTERED, SENT,
- * QUEUED, ERROR)
- *
- * Send messages, assert that: - The source connector stats have TEST_SIZE for RECEIVED and
- * TRANSFORMED - The first destination connector stats have TEST_SIZE for FILTERED - The second
- * destination connector stats have TEST_SIZE for SENT - The third destination connector stats
- * have TEST_SIZE for QUEUED - The fourth destination connector stats have TEST_SIZE for ERROR -
- * The aggregate stats are correct (RECEIVED/TRANSFORMED is the same as the source connector,
- * PENDING/SENT/QUEUED is the same as the destination connectors combined, FILTERED/ERROR is the
- * same as all connectors combined)
- */
- @Test
- public final void testStatistics3() throws Exception {
- TestUtils.initChannel(channelId);
-
- TestChannel channel = new TestChannel();
-
- channel.setChannelId(channelId);
- channel.setServerId(serverId);
-
- channel.setPreProcessor(new TestPreProcessor());
- channel.setPostProcessor(new TestPostProcessor());
-
- TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector();
- sourceConnector.setChannelId(channel.getChannelId());
- sourceConnector.setChannel(channel);
- channel.setSourceConnector(sourceConnector);
- channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
-
- DestinationChainProvider chain = new DestinationChainProvider();
- chain.setChannelId(channel.getChannelId());
-
- for (int i = 1; i <= 4; i++) {
- DestinationConnector destinationConnector = new TestDispatcher();
- destinationConnector.setChannelId(channel.getChannelId());
-
- TestDispatcherProperties connectorProperties = new TestDispatcherProperties();
- if (i == 3) {
- connectorProperties.getDestinationConnectorProperties().setQueueEnabled(true);
- connectorProperties.getDestinationConnectorProperties().setSendFirst(false);
- } else {
- connectorProperties.getDestinationConnectorProperties().setQueueEnabled(false);
- }
-
- destinationConnector.setConnectorProperties(connectorProperties);
- destinationConnector.setDestinationName(TestUtils.DEFAULT_DESTINATION_NAME);
- destinationConnector.setInboundDataType(new TestDataType());
- destinationConnector.setOutboundDataType(new TestDataType());
- destinationConnector.setResponseTransformerExecutor(TestUtils.createDefaultResponseTransformerExecutor());
-
- DestinationQueue destinationConnectorQueue = new DestinationQueue(connectorProperties.getDestinationConnectorProperties().getThreadAssignmentVariable(), connectorProperties.getDestinationConnectorProperties().getThreadCount(), connectorProperties.getDestinationConnectorProperties().isRegenerateTemplate(), destinationConnector.getSerializer(), destinationConnector.getMessageMaps());
- destinationConnectorQueue.setDataSource(new ConnectorMessageQueueDataSource(channel.getChannelId(), channel.getServerId(), i, Status.QUEUED, false, daoFactory));
- destinationConnectorQueue.updateSize();
- destinationConnector.setQueue(destinationConnectorQueue);
-
- FilterTransformerExecutor filterTransformerExecutor = TestUtils.createDefaultFilterTransformerExecutor();
-
- switch (i) {
- case 1:
- ((TestFilterTransformer) filterTransformerExecutor.getFilterTransformer()).setFiltered(true);
- break;
- case 2:
- ((TestDispatcher) destinationConnector).setReturnStatus(Status.SENT);
- break;
- case 3:
- ((TestDispatcher) destinationConnector).setReturnStatus(Status.QUEUED);
- break;
- case 4:
- ((TestDispatcher) destinationConnector).setReturnStatus(Status.ERROR);
- break;
- }
-
- destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer());
- destinationConnector.setMetaDataColumns(channel.getMetaDataColumns());
- destinationConnector.setFilterTransformerExecutor(filterTransformerExecutor);
-
- chain.addDestination(i, destinationConnector);
- }
-
- channel.addDestinationChainProvider(chain);
-
- ChannelController.getInstance().deleteAllMessages(channel.getChannelId());
- TestUtils.deleteChannelStatistics(channel.getChannelId());
- channel.deploy();
- channel.start(null);
-
- // Send messages through the channel
- for (int i = 1; i <= TEST_SIZE; i++) {
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
- }
-
- // Assert that the messages were sent
- assertEquals(TEST_SIZE, channel.getNumMessages());
-
- // Assert the source connector stats are correct
- statsEqual(channel.getChannelId(), 0, TEST_SIZE, 0L, TEST_SIZE, 0L, 0L, 0L);
- // Assert that destination connector stats are correct
- statsEqual(channel.getChannelId(), 1, TEST_SIZE, TEST_SIZE, 0L, 0L, 0L, 0L);
- statsEqual(channel.getChannelId(), 2, TEST_SIZE, 0L, 0L, 0L, TEST_SIZE, 0L);
- statsEqual(channel.getChannelId(), 3, TEST_SIZE, 0L, 0L, 0L, 0L, 0L);
- statsEqual(channel.getChannelId(), 4, TEST_SIZE, 0L, 0L, 0L, 0L, TEST_SIZE);
- // Assert the aggregate stats are correct
- assertTrue(channelStatsCorrect());
-
- channel.stop();
- channel.undeploy();
-
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- /*
- * Create a channel where the destination connector sends messages as usual, but both the
- * dispatcher's send method and the response transformer's doTransform method stall the
- * processing thread for a specified amount of time. This way, we can check at specific
- * intervals whether the message statistics in the database have been updated to QUEUED,
- * PENDING, and the final return status.
- *
- * Send messages, and for each message: Wait a specified amount of time, then assert: - The
- * destination connector stats have 1 for QUEUED, the total number of messages for RECEIVED, and
- * the remainder for the final return status.
- *
- * Wait a specified amount of time, then assert: - The destination connector stats have 1 for
- * PENDING, the total number of messages for RECEIVED, and the remainder for the final return
- * status.
- *
- * Wait a specified amount of time, then assert: - The source connector stats have the total
- * number of messages for RECEIVED and TRANSFORMED - The destination connector stats have the
- * total number of messages for RECEIVED and the final return status. - The aggregate stats are
- * correct
- *
- * After all messages finish processing, assert: - The source connector stats have testSize for
- * RECEIVED and TRANSFORMED - The destination connector stats have testSize for RECEIVED and the
- * final return status - The aggregate stats are correct (RECEIVED/TRANSFORMED is the same as
- * the source connector, PENDING/SENT/QUEUED is the same as the destination connectors combined,
- * FILTERED/ERROR is the same as all connectors combined)
- */
- @Test
- public final void testStatistics4() throws Exception {
- final int waitTime = 1000;
- long testSize = 5;
- Status returnStatus = Status.SENT;
-
- TestUtils.initChannel(channelId);
-
- TestChannel channel = new TestChannel();
-
- channel.setChannelId(channelId);
- channel.setServerId(serverId);
-
- channel.setPreProcessor(new TestPreProcessor());
- channel.setPostProcessor(new TestPostProcessor());
-
- TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector();
- sourceConnector.setChannelId(channel.getChannelId());
- sourceConnector.setChannel(channel);
- channel.setSourceConnector(sourceConnector);
- channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
-
- DestinationChainProvider chain = new DestinationChainProvider();
- chain.setChannelId(channel.getChannelId());
-
- class BlockingTestDispatcher extends TestDispatcher {
- public volatile boolean waiting = true;
-
- @Override
- public Response send(ConnectorProperties connectorProperties, ConnectorMessage message) {
- while (waiting) {
- try {
- Thread.sleep(waitTime);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- return super.send(connectorProperties, message);
- }
- }
-
- // Create a destination connector that stalls the processing thread a specified amount of time during the send method
- BlockingTestDispatcher destinationConnector = new BlockingTestDispatcher();
- destinationConnector.setChannelId(channel.getChannelId());
-
- destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer());
- destinationConnector.setMetaDataColumns(channel.getMetaDataColumns());
- destinationConnector.setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor());
-
- TestDispatcherProperties connectorProperties = new TestDispatcherProperties();
- connectorProperties.getDestinationConnectorProperties().setQueueEnabled(true);
- connectorProperties.getDestinationConnectorProperties().setSendFirst(false);
-
- destinationConnector.setConnectorProperties(connectorProperties);
- destinationConnector.setDestinationName(TestUtils.DEFAULT_DESTINATION_NAME);
- destinationConnector.setInboundDataType(new TestDataType());
- destinationConnector.setOutboundDataType(new TestDataType());
- destinationConnector.setResponseTransformerExecutor(TestUtils.createDefaultResponseTransformerExecutor());
-
- Map params = new HashMap();
- params.put("localChannelId", ChannelController.getInstance().getLocalChannelId(channel.getChannelId()));
- params.put("channelId", channel.getChannelId());
- params.put("metaDataId", 1);
- params.put("status", Status.QUEUED);
-
- DestinationQueue destinationConnectorQueue = new DestinationQueue(connectorProperties.getDestinationConnectorProperties().getThreadAssignmentVariable(), connectorProperties.getDestinationConnectorProperties().getThreadCount(), connectorProperties.getDestinationConnectorProperties().isRegenerateTemplate(), destinationConnector.getSerializer(), destinationConnector.getMessageMaps());
- destinationConnectorQueue.setDataSource(new ConnectorMessageQueueDataSource(channel.getChannelId(), channel.getServerId(), 1, Status.QUEUED, false, daoFactory));
- destinationConnector.setQueue(destinationConnectorQueue);
-
- ((TestDispatcher) destinationConnector).setReturnStatus(returnStatus);
-
- class BlockingTestResponseTransformer extends TestResponseTransformer {
- public volatile boolean waiting = true;
-
- @Override
- public String doTransform(Response response, ConnectorMessage connectorMessage) throws DonkeyException, InterruptedException {
- while (waiting) {
- try {
- Thread.sleep(waitTime);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- return super.doTransform(response, connectorMessage);
- }
- }
-
- // Create a response transformer that stalls the processing thread a specified amount of time
- BlockingTestResponseTransformer responseTransformer = new BlockingTestResponseTransformer();
- destinationConnector.getResponseTransformerExecutor().setResponseTransformer(responseTransformer);
-
- chain.addDestination(1, destinationConnector);
-
- channel.addDestinationChainProvider(chain);
-
- ChannelController.getInstance().deleteAllMessages(channel.getChannelId());
- TestUtils.deleteChannelStatistics(channel.getChannelId());
- channel.deploy();
- channel.start(null);
-
- Map stats;
-
- logger.info(String.format("%-140s", "Testing destination connector statistics changing: RECEIVED->QUEUED->PENDING->SENT"));
- logger.info(String.format("%-140s", "Test size: " + testSize));
- logger.info(String.format("%-140s", ""));
-
- logger.info(String.format("%-140s", "Destination Connector Stats"));
- logger.info(String.format("%-140s", "---------------------------"));
-
- // Send messages through the channel
- for (long i = 1; i <= testSize; i++) {
- destinationConnector.waiting = true;
- responseTransformer.waiting = true;
-
- logger.info(String.format("%-140s", "Sending Message #" + i + "..."));
- ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage);
-
- logger.info(String.format("%-140s", String.format("%-50s", " - After sending (should be QUEUED): ") + (stats = ChannelController.getInstance().getStatistics().getConnectorStats(channelId, 1))));
-
- // Assert that destination connector stats are correct
- assertDestinationStatsCorrect(stats, i, returnStatus, Status.QUEUED);
-
- destinationConnector.waiting = false;
- logger.info(String.format("%-140s", " - Waiting " + waitTime + " ms..."));
- Thread.sleep(waitTime);
-
- logger.info(String.format("%-140s", String.format("%-50s", " - After first wait (should be PENDING): ") + (stats = ChannelController.getInstance().getStatistics().getConnectorStats(channelId, 1))));
-
- // Assert that destination connector stats are correct
- assertDestinationStatsCorrect(stats, i, returnStatus, Status.PENDING);
-
- responseTransformer.waiting = false;
- logger.info(String.format("%-140s", " - Waiting " + waitTime + " ms..."));
- Thread.sleep(waitTime);
-
- logger.info(String.format("%-140s", String.format("%-50s", " - After second wait (should be " + returnStatus + "): ") + (stats = ChannelController.getInstance().getStatistics().getConnectorStats(channelId, 1))));
-
- // Assert the source connector stats are correct
- statsEqual(channel.getChannelId(), 0, i, 0L, i, 0L, 0L, 0L);
-
- // Assert that destination connector stats are correct
- assertDestinationStatsCorrect(stats, i, returnStatus, returnStatus);
-
- // Assert the aggregate stats are correct
- assertTrue(channelStatsCorrect());
- }
-
- // Assert that the messages were sent
- assertEquals(testSize, channel.getNumMessages());
-
- // Assert the source connector stats are correct
- statsEqual(channel.getChannelId(), 0, testSize, 0L, testSize, 0L, 0L, 0L);
- // Assert that destination connector stats are correct
- destinationStatsCorrect(channel.getChannelId(), 1, testSize, returnStatus, returnStatus);
- // Assert the aggregate stats are correct
- assertTrue(channelStatsCorrect());
-
- channel.stop();
- channel.undeploy();
-
- ChannelController.getInstance().removeChannel(channel.getChannelId());
- }
-
- private void statsEqual(String channelId, Integer metaDataId, Long received, Long filtered, Long transformed, Long pending, Long sent, Long error) {
- assertStatsEqual(ChannelController.getInstance().getStatistics().getConnectorStats(channelId, metaDataId), received, filtered, transformed, pending, sent, error);
- }
-
- private void assertStatsEqual(Map stats, Long received, Long filtered, Long transformed, Long pending, Long sent, Long error) {
- assertEquals(received, stats.get(Status.RECEIVED));
- assertEquals(filtered, stats.get(Status.FILTERED));
- assertEquals(transformed, stats.get(Status.TRANSFORMED));
- assertEquals(pending, stats.get(Status.PENDING));
- assertEquals(sent, stats.get(Status.SENT));
- assertEquals(error, stats.get(Status.ERROR));
- }
-
- /*
- * Gets the aggregate channel statistics from storage and checks whether they are correct with
- * respect to each individual connector. Although the TRANSFORMED status is updated in the
- * channel stats along with both source and destination connectors (in ChannelStatistics), the
- * destination connector message status is never updated in the database as TRANSFORMED after it
- * passes through the filter/transformer. So here, we only check the aggregate TRANSFORMED
- * statistics with respect to the source connector.
- */
- private boolean channelStatsCorrect() {
- Map> stats = ChannelController.getInstance().getStatistics().getChannelStats(channelId);
- Map channelStats = createStatsMap();
-
- for (Integer metaDataId : stats.keySet()) {
- if (metaDataId != null) {
- for (Status status : stats.get(metaDataId).keySet()) {
- switch (status) {
- // Aggregate RECEIVED and TRANSFORMED stats should be the same as the source connector
- case RECEIVED:
- case TRANSFORMED:
- if (metaDataId == 0) {
- channelStats.put(status, channelStats.get(status) + stats.get(metaDataId).get(status));
- }
- break;
-
- // Aggregate FILTERED and ERROR stats should be the same as all connectors combined
- case FILTERED:
- case ERROR:
- channelStats.put(status, channelStats.get(status) + stats.get(metaDataId).get(status));
- break;
-
- // Aggregate PENDING, SENT, and QUEUED stats should be the same as the destination connector
- case PENDING:
- case SENT:
- case QUEUED:
- if (metaDataId > 0) {
- channelStats.put(status, channelStats.get(status) + stats.get(metaDataId).get(status));
- }
- break;
- }
- }
- }
- }
-
- return channelStats.equals(stats.get(null));
- }
-
- private void destinationStatsCorrect(String channelId, int metaDataId, long numMessages, Status returnStatus, Status currentStatus) {
- assertDestinationStatsCorrect(ChannelController.getInstance().getStatistics().getConnectorStats(channelId, metaDataId), numMessages, returnStatus, currentStatus);
- }
-
- private void assertDestinationStatsCorrect(Map stats, long numMessages, Status returnStatus, Status currentStatus) {
- long pending = 0;
- long remainder = numMessages;
- switch (currentStatus) {
- case PENDING:
- pending = 1;
- remainder--;
- break;
- case QUEUED:
- remainder--;
- break;
- default:
- break;
- }
-
- switch (returnStatus) {
- case FILTERED:
- assertStatsEqual(stats, numMessages, remainder, 0L, pending, 0L, 0L);
- break;
-
- case SENT:
- assertStatsEqual(stats, numMessages, 0L, 0L, pending, remainder, 0L);
- break;
-
- case QUEUED:
- assertStatsEqual(stats, numMessages, 0L, 0L, pending, 0L, 0L);
- break;
-
- case ERROR:
- assertStatsEqual(stats, numMessages, 0L, 0L, pending, 0L, remainder);
- break;
- }
- }
-
- private Map createStatsMap() {
- Map connectorStats = new HashMap();
- connectorStats.put(Status.RECEIVED, 0L);
- connectorStats.put(Status.FILTERED, 0L);
- connectorStats.put(Status.TRANSFORMED, 0L);
- connectorStats.put(Status.PENDING, 0L);
- connectorStats.put(Status.SENT, 0L);
- connectorStats.put(Status.ERROR, 0L);
-
- return connectorStats;
- }
-}
\ No newline at end of file
diff --git a/server/conf/mirth.properties b/server/conf/mirth.properties
index 6b71ebdbf3..e258dd0261 100644
--- a/server/conf/mirth.properties
+++ b/server/conf/mirth.properties
@@ -80,7 +80,7 @@ database = derby
# examples:
# Derby jdbc:derby:${dir.appdata}/mirthdb;create=true
# PostgreSQL jdbc:postgresql://localhost:5432/mirthdb
-# MySQL jdbc:mysql://localhost:3306/mirthdb
+# MySQL jdbc:mysql://localhost:3306/mirthdb?sessionVariables=transaction_isolation='READ-COMMITTED'
# Oracle jdbc:oracle:thin:@localhost:1521:DB
# SQL Server/Sybase (jTDS) jdbc:jtds:sqlserver://localhost:1433/mirthdb
# Microsoft SQL Server jdbc:sqlserver://localhost:1433;databaseName=mirthdb
diff --git a/server/src/main/java/com/mirth/connect/client/core/Client.java b/server/src/main/java/com/mirth/connect/client/core/Client.java
index a927508d67..e5d0a3688e 100644
--- a/server/src/main/java/com/mirth/connect/client/core/Client.java
+++ b/server/src/main/java/com/mirth/connect/client/core/Client.java
@@ -809,6 +809,16 @@ public void setConfigurationMap(Map map) throws C
getServlet(ConfigurationServletInterface.class).setConfigurationMap(map);
}
+ /**
+ * Updates a single entry in the configuration map, leaving the rest alone.
+ *
+ * @see ConfigurationServletInterface#setConfigurationProperty
+ */
+ @Override
+ public void setConfigurationProperty(String key, ConfigurationProperty property) throws ClientException {
+ getServlet(ConfigurationServletInterface.class).setConfigurationProperty(key, property);
+ }
+
/**
* Returns the database driver list.
*
diff --git a/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java b/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java
index 9f8d391bfe..4c9ca4907e 100644
--- a/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java
+++ b/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java
@@ -306,6 +306,14 @@ public void setConfigurationMap(@Param("map") @RequestBody(description = "The ne
@Content(mediaType = MediaType.APPLICATION_JSON, examples = {
@ExampleObject(name = "configurationMap", ref = "../apiexamples/configuration_map_json") }) }) Map map) throws ClientException;
+ @PUT
+ @Path("/configurationMap/{key}")
+ @Operation(summary = "Updates a single entry in the configuration map, leaving the rest alone.")
+ @MirthOperation(name = "setConfigurationProperty", display = "Set configuration map entry", permission = Permissions.CONFIGURATION_MAP_EDIT)
+ public void setConfigurationProperty(
+ @Param("key") @Parameter(description = "The key of the entry to set.", required = true) @PathParam("key") String key,
+ @Param("property") @RequestBody(description = "The value and comment to store under the key.", required = true) ConfigurationProperty property) throws ClientException;
+
@GET
@Path("/databaseDrivers")
@Operation(summary = "Returns the database driver list.")
diff --git a/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java b/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java
index 48467e6598..6f69628e7d 100644
--- a/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java
+++ b/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java
@@ -105,6 +105,11 @@ public ChannelStatistics getStatistics(String channelId) {
if (CollectionUtils.isNotEmpty(channelStatisticsList)) {
channelStatistics = channelStatisticsList.get(0);
+
+ // Paranoia: verify that the channel ID matches the requested one
+ if (!channelId.equals(channelStatistics.getChannelId())) {
+ throw new IllegalStateException("Channel ID mismatch: expected " + channelId + " but got " + channelStatistics.getChannelId());
+ }
} else {
channelStatistics = new ChannelStatistics();
channelStatistics.setChannelId(channelId);
diff --git a/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java b/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java
index 67dd3419fc..e58740be3a 100644
--- a/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java
+++ b/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java
@@ -306,6 +306,15 @@ public void setConfigurationMap(Map map) {
}
}
+ @Override
+ public void setConfigurationProperty(String key, ConfigurationProperty property) {
+ try {
+ configurationController.setConfigurationProperty(key, property);
+ } catch (ControllerException e) {
+ throw new MirthApiException(e);
+ }
+ }
+
@Override
public List getDatabaseDrivers() {
try {
diff --git a/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java b/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java
index b7897771c7..21b9eede6a 100644
--- a/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java
+++ b/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java
@@ -86,8 +86,12 @@ public static ChannelController getInstance() {
public abstract Statistics getStatisticsFromStorage(String serverId);
+ public abstract Statistics getStatisticsFromStorage(String serverId, Set channelIds);
+
public abstract Statistics getTotalStatisticsFromStorage(String serverId);
+ public abstract Statistics getTotalStatisticsFromStorage(String serverId, Set channelIds);
+
public abstract int getConnectorMessageCount(String channelId, String serverId, int metaDataId, Status status);
public abstract void resetStatistics(Map> channelConnectorMap, Set statuses);
diff --git a/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java b/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java
index e4a9ce88c3..7aa1d49dd1 100644
--- a/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java
+++ b/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java
@@ -328,6 +328,19 @@ public static ConfigurationController getInstance() {
*/
public abstract void setConfigurationProperties(Map map, boolean persist) throws ControllerException;
+ /**
+ * Sets a single configuration map entry, leaving every other entry alone. Doing the
+ * read-modify-write here rather than in the caller is what makes it atomic: a client can only
+ * replace the whole map, so two clients updating different keys at once would otherwise drop
+ * each other's writes.
+ *
+ * @param key
+ * The key of the entry to set.
+ * @param property
+ * The value and comment to store under the key.
+ */
+ public abstract void setConfigurationProperty(String key, ConfigurationProperty property) throws ControllerException;
+
// properties
public Properties getPropertiesForGroup(String group) {
return getPropertiesForGroup(group, null);
diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java b/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java
index 2dba0303e5..b42aeb996f 100644
--- a/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java
+++ b/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java
@@ -732,11 +732,21 @@ public Statistics getStatisticsFromStorage(String serverId) {
return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getStatisticsFromStorage(serverId);
}
+ @Override
+ public Statistics getStatisticsFromStorage(String serverId, Set channelIds) {
+ return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getStatisticsFromStorage(serverId, channelIds);
+ }
+
@Override
public Statistics getTotalStatisticsFromStorage(String serverId) {
return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getTotalStatisticsFromStorage(serverId);
}
+ @Override
+ public Statistics getTotalStatisticsFromStorage(String serverId, Set channelIds) {
+ return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getTotalStatisticsFromStorage(serverId, channelIds);
+ }
+
@Override
public int getConnectorMessageCount(String channelId, String serverId, int metaDataId, Status status) {
return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getConnectorMessageCount(channelId, serverId, metaDataId, status);
diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java
index bfee2eddbf..bc01bd9e94 100644
--- a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java
+++ b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java
@@ -938,6 +938,13 @@ public synchronized void setConfigurationProperties(Map properties = getConfigurationProperties();
+ properties.put(key, property);
+ setConfigurationProperties(properties, true);
+ }
+
@Override
public void setStatus(int status) {
this.status = status;
diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java b/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java
index 1dbeb66b03..19d4a3f947 100644
--- a/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java
+++ b/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java
@@ -739,7 +739,7 @@ private Map getDashboardChannels(Set channelIds) {
synchronized (deployingChannels) {
for (Channel channel : deployingChannels) {
- if (!channels.containsKey(channel.getChannelId())) {
+ if (requested(channelIds, channel) && !channels.containsKey(channel.getChannelId())) {
channels.put(channel.getChannelId(), channel);
}
}
@@ -747,7 +747,7 @@ private Map getDashboardChannels(Set channelIds) {
synchronized (undeployingChannels) {
for (Channel channel : undeployingChannels) {
- if (!channels.containsKey(channel.getChannelId())) {
+ if (requested(channelIds, channel) && !channels.containsKey(channel.getChannelId())) {
channels.put(channel.getChannelId(), channel);
}
}
@@ -755,6 +755,11 @@ private Map getDashboardChannels(Set channelIds) {
return channels;
}
+ /** Whether one channel is in the set a caller asked about; an empty set means all of them. */
+ private boolean requested(Set channelIds, Channel channel) {
+ return CollectionUtils.isEmpty(channelIds) || channelIds.contains(channel.getChannelId());
+ }
+
@Override
public List getChannelStatusList(Set channelIds, boolean includeUndeployed) {
List statusList = new ArrayList<>();
@@ -778,8 +783,16 @@ public List getChannelStatusList(Set channelIds, boolea
private List getUndeployedDashboardStatuses(Collection channelModels, Map metadataMap) {
List statuses = new ArrayList();
- Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId());
- Statistics lifetimeStats = channelController.getTotalStatisticsFromStorage(configurationController.getServerId());
+ if (channelModels.isEmpty()) {
+ return statuses;
+ }
+
+ Set channelIds = new HashSet();
+ for (com.mirth.connect.model.Channel channelModel : channelModels) {
+ channelIds.add(channelModel.getId());
+ }
+ Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId(), channelIds);
+ Statistics lifetimeStats = channelController.getTotalStatisticsFromStorage(configurationController.getServerId(), channelIds);
String serverId = configurationController.getServerId();
for (com.mirth.connect.model.Channel channelModel : channelModels) {
@@ -1043,7 +1056,15 @@ private List getDashboardChannelStatistics(Collection getUndeployedChannelStatistics(Collection channelModels, Set includeMetaDataIds, Set excludeMetaDataIds) {
List statisticsList = new ArrayList();
- Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId());
+ if (channelModels.isEmpty()) {
+ return statisticsList;
+ }
+
+ Set channelIds = new HashSet();
+ for (com.mirth.connect.model.Channel channelModel : channelModels) {
+ channelIds.add(channelModel.getId());
+ }
+ Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId(), channelIds);
String serverId = configurationController.getServerId();
diff --git a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerailizerTests.java b/server/src/test/java/com/mirth/connect/model/converters/DocumentSerailizerTests.java
deleted file mode 100644
index 72e6fa552e..0000000000
--- a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerailizerTests.java
+++ /dev/null
@@ -1,70 +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.model.converters;
-
-import javax.xml.parsers.DocumentBuilderFactory;
-
-import junit.framework.Assert;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-public class DocumentSerailizerTests {
-
- @Before
- public void setUp() throws Exception {}
-
- @After
- public void tearDown() throws Exception {}
-
- @Test
- public void testToXML() throws Exception {
- DocumentSerializer serializer = new DocumentSerializer();
- Document document = getSecureDocumentBuilderFactory();
-
- Element element = document.createElement("root");
- element.setTextContent("Hello\r\nworld!");
- document.appendChild(element);
-
- String actual = serializer.toXML(document);
- String expected = "\nHello
\nworld!\n";
- Assert.assertEquals(expected, actual);
- }
-
- @Test
- public void testPreserveSpace() throws Exception {
- Document document = getSecureDocumentBuilderFactory();
- Element root = document.createElement("root");
- document.appendChild(root);
- Element child = document.createElement("child");
- child.setTextContent("Hello\nworld!");
- root.appendChild(child);
-
- DocumentSerializer serializer = new DocumentSerializer();
- String actual = serializer.toXML(document);
- String expected = "\n\nHello\nworld!\n\n";
- Assert.assertEquals(expected, actual);
- }
-
- @Test
- public void testFromXML() {
-
- }
-
- private static Document getSecureDocumentBuilderFactory() throws Exception {
- DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
- dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
- return dbf.newDocumentBuilder().newDocument();
- }
-
-}
diff --git a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java b/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java
index 2e8b08a202..1ab7972040 100644
--- a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java
+++ b/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java
@@ -1,5 +1,6 @@
package com.mirth.connect.model.converters;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -22,6 +23,7 @@
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3c.dom.Document;
+import org.w3c.dom.Element;
import org.xml.sax.InputSource;
public class DocumentSerializerTest {
@@ -87,6 +89,34 @@ public void testValidFromXML() throws Exception {
assertNotNull(document);
}
+ @Test
+ public void testToXML() throws Exception {
+ Document document = newDocument();
+ Element root = document.createElement("root");
+ root.setTextContent("Hello\r\nworld!");
+ document.appendChild(root);
+
+ // the carriage return survives as a character reference, the line feed as itself
+ assertEquals("\nHello
\nworld!\n", serializer.toXML(document));
+ }
+
+ @Test
+ public void testPreserveSpace() throws Exception {
+ Document document = newDocument();
+ Element root = document.createElement("root");
+ document.appendChild(root);
+ Element child = document.createElement("child");
+ child.setTextContent("Hello\nworld!");
+ root.appendChild(child);
+
+ // the element gets indented, but the line feed inside the value is left alone
+ assertEquals("\n\n Hello\nworld!\n\n", serializer.toXML(document));
+ }
+
+ private static Document newDocument() throws Exception {
+ return DocumentSerializer.getSecureDocumentBuilderFactory().newDocumentBuilder().newDocument();
+ }
+
// Util method for debugging purposes
@SuppressWarnings("unused")
private static void printDocument(Document doc) throws IOException, TransformerException {
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/model/util/MigrationUtilTests.java b/server/src/test/java/com/mirth/connect/model/util/MigrationUtilTest.java
similarity index 72%
rename from server/src/test/java/com/mirth/connect/model/util/MigrationUtilTests.java
rename to server/src/test/java/com/mirth/connect/model/util/MigrationUtilTest.java
index 8935c2abe4..d4145c6165 100644
--- a/server/src/test/java/com/mirth/connect/model/util/MigrationUtilTests.java
+++ b/server/src/test/java/com/mirth/connect/model/util/MigrationUtilTest.java
@@ -13,7 +13,7 @@
import com.mirth.connect.util.MigrationUtil;
-public class MigrationUtilTests extends TestCase {
+public class MigrationUtilTest extends TestCase {
public void testCompareVersions() {
assertEquals(1, MigrationUtil.compareVersions("5", "4"));
assertEquals(-1, MigrationUtil.compareVersions("5", "6"));
@@ -31,10 +31,15 @@ public void testCompareVersions() {
assertEquals(-1, MigrationUtil.compareVersions("1.8", "1.8.2"));
}
+ /*
+ * length is the exact number of components to emit: shorter versions are padded with zeroes and
+ * longer ones are truncated, so a length below the number of components present loses them and
+ * a non-positive length yields nothing at all. Both production callers pass 3.
+ */
public void testNormalizeVersion() {
- assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", -1));
- assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", 0));
- assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", 1));
+ assertEquals("", MigrationUtil.normalizeVersion("1.8", -1));
+ assertEquals("", MigrationUtil.normalizeVersion("1.8", 0));
+ assertEquals("1", MigrationUtil.normalizeVersion("1.8", 1));
assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", 2));
assertEquals("1.8.0", MigrationUtil.normalizeVersion("1.8", 3));
assertEquals("1.8.0.0", MigrationUtil.normalizeVersion("1.8", 4));
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 030158e228..7c1819a7e2 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 {
@@ -41,6 +45,36 @@ public void testFromXMLWithExternalDtd() {
assertTrue(exceptionCaught);
}
+ @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-1597-output.txt"), serializer.fromXML(read("test-1597-input-missing-elements.xml")));
+ }
+
+ @Test
+ public void testIssue1597toXML() throws Exception {
+ assertXmlEquals(read("test-1597-input.xml"), serializer.toXML(read("test-1597-output.txt")));
+ }
+
+ 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/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/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/server/util/PasswordRequirementsTests.java b/server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTest.java
similarity index 97%
rename from server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTests.java
rename to server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTest.java
index e21b4a683e..66494fdd96 100644
--- a/server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTests.java
+++ b/server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTest.java
@@ -14,7 +14,7 @@
import com.mirth.connect.client.core.ControllerException;
import com.mirth.connect.model.PasswordRequirements;
-public class PasswordRequirementsTests extends TestCase {
+public class PasswordRequirementsTest extends TestCase {
protected void setUp() throws Exception {
super.setUp();
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/server/util/TemplateValueReplacerTests.java b/server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTest.java
similarity index 98%
rename from server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTests.java
rename to server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTest.java
index d763a136e8..3c45cb7f52 100644
--- a/server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTests.java
+++ b/server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTest.java
@@ -19,7 +19,7 @@
import org.junit.Before;
import org.junit.Test;
-public class TemplateValueReplacerTests {
+public class TemplateValueReplacerTest {
private TemplateValueReplacer templateReplacer;
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..b135d29ac3
--- /dev/null
+++ b/server/tests/dicom/README.md
@@ -0,0 +1,22 @@
+# DICOM test samples
+
+All CC-BY licensed (commercial reuse OK with attribution), sourced from NCI Imaging Data Commons,
+by way of https://saga-it.com/dicom/samples
+
+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 |
+| --- | --- | --- |
+| `ct-abdomen-c4kc-kits-instance` | CT | C4KC-KiTS |
+| `ct-chest-lidc-idri-instance` | CT | LIDC-IDRI |
+| `ct-lung-screening-nlst-instance` | CT | NLST |
+| `mg-mammography-cbis-ddsm-instance` | MG | CBIS-DDSM |
+| `mr-prostate-prostatex-instance` | MR | PROSTATEx |
+| `pt-breast-qin-01-instance` | PT | QIN-BREAST |
+| `us-lymph-node-cmb-lca-instance` | US | CMB-LCA |
+
+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-1597-input.xml b/server/tests/test-1597-input.xml
new file mode 100644
index 0000000000..0a7ded8aeb
--- /dev/null
+++ b/server/tests/test-1597-input.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-1597-output.txt b/server/tests/test-1597-output.txt
new file mode 100644
index 0000000000..e1a30e694e
--- /dev/null
+++ b/server/tests/test-1597-output.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-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
+
+
+
+
diff --git a/smoketest/build.gradle b/smoketest/build.gradle
index 4d121012c8..18e8b941bc 100644
--- a/smoketest/build.gradle
+++ b/smoketest/build.gradle
@@ -30,13 +30,26 @@ sourceSets {
}
}
+// match ci/run-harness.sh, sort is critical
+def engineHome = project(':server').file('setup')
+def engineJars = files({
+ fileTree(engineHome) {
+ include 'extensions/**/*.jar'
+ include 'server-lib/**/*.jar'
+ }.files.sort { it.path }
+})
+
// The root convention applies useJUnit() to every Test task; this module is JUnit 5.
tasks.named('test') {
useJUnitPlatform()
// Only run our tests if a live server URL is provided
// `./gradlew :smoketest:test -Doie.baseUrl=https://localhost:8443`
onlyIf { System.getProperty('oie.baseUrl') != null }
+ // The thing under test is a running server, no incremental build is possible.
+ outputs.upToDateWhen { false }
systemProperties System.getProperties().findAll { it.key.toString().startsWith('oie.') }
+ // Engine jars first, harness classes and fixtures last, as in the container.
+ classpath = engineJars + classpath
}
tasks.named('jar', Jar) {
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/BlockingResponseTransformerTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/BlockingResponseTransformerTest.java
new file mode 100644
index 0000000000..0e9e87e88b
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/BlockingResponseTransformerTest.java
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.LinkedHashMap;
+import java.util.UUID;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.donkey.model.message.ConnectorMessage;
+import com.mirth.connect.donkey.model.message.Status;
+
+/**
+ * A destination whose response transformer has not returned yet, the one part of
+ * ci/tests/190-response-handling that is not a fixture: the harness retries every fixture
+ * assertion until the message is terminal, so it can never see the PENDING the engine parks
+ * the destination at while the transformer runs.
+ *
+ * Nothing here waits on a clock. The channel queues at its source, so the submit returns
+ * the message id straight away, and its response transformer parks until the gate property
+ * holds that message's token. The token is fresh per run, so a gate left set by an earlier run
+ * cannot release this one.
+ */
+@DisplayName("190-response-handling/06-blocking-response-transformer")
+@TestInstance(PER_CLASS)
+class BlockingResponseTransformerTest {
+
+ private static final String CHANNEL = "channels/blocking-response-transformer.xml";
+
+ /** The configuration property the response transformer parks on. */
+ private static final String GATE = "oie.smoketest.releaseResponse";
+
+ private static final int DESTINATION = 1;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void destinationStaysPendingUntilTheResponseTransformerReturns() throws Exception {
+ // The payload is this run's token: the destination echoes it back, so it is also the
+ // response the transformer parks on and the content it rewrites.
+ String token = UUID.randomUUID().toString();
+ long messageId = SharedServer.get().submitMessage(channelId, token, new LinkedHashMap<>());
+
+ try {
+ ConnectorMessage blocked = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.PENDING);
+ assertNotNull(blocked.getSent(),
+ "the destination sent its message before the response transformer ran, so the"
+ + " sent content should already be stored alongside the PENDING status");
+ assertNull(blocked.getProcessedResponse(),
+ "the response transformer has not returned, so there is no processed response yet");
+ } finally {
+ // Also on failure, so a wedged transformer never outlives the test.
+ Harness.setConfigurationProperty(GATE, token);
+ }
+
+ ConnectorMessage finished = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT);
+ assertNotNull(finished.getProcessedResponse(), "no processed response was stored");
+ assertEquals("released<" + token + ">",
+ MessageAssertions.responsePayload(finished.getProcessedResponse().getContent()),
+ "the response transformer's output should be the destination's processed response");
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/CustomMetaDataColumnRedeployTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/CustomMetaDataColumnRedeployTest.java
new file mode 100644
index 0000000000..ea4b5acb5a
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/CustomMetaDataColumnRedeployTest.java
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.donkey.model.channel.MetaDataColumn;
+import com.mirth.connect.donkey.model.channel.MetaDataColumnType;
+import com.mirth.connect.donkey.model.message.Status;
+import com.mirth.connect.model.Channel;
+import com.mirth.connect.model.converters.ObjectXMLSerializer;
+
+/**
+ * The half of custom metadata columns that ci/tests/200-custom-metadata-columns cannot express: a
+ * fixture deploys each channel once, so it only ever reaches {@code addMetaDataColumn}. Editing a
+ * deployed channel's column list and redeploying it is what reaches {@code removeMetaDataColumn},
+ * and what turns a column's type change into a drop followed by an add.
+ */
+@DisplayName("200-custom-metadata-columns/02-redeploy")
+@TestInstance(PER_CLASS)
+class CustomMetaDataColumnRedeployTest {
+
+ private static final String CHANNEL = "channels/custom-metadata-columns-redeploy.xml";
+
+ private String channelId;
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void redeployAddsRemovesAndRetypesColumns() throws Exception {
+ OieServer server = SharedServer.get();
+ Channel channel = ObjectXMLSerializer.getInstance().deserialize(Harness.resource(CHANNEL), Channel.class);
+
+ // The channel starts with KEPT, DROPPED and RETYPED, all STRING.
+ channelId = server.deployChannel(channel, CHANNEL);
+ long first = submit(server, "kept", "one", "dropped", "one", "retyped", "1.5");
+
+ Map firstMetaData = metaData(first);
+ assertEquals("one", firstMetaData.get("KEPT"));
+ assertEquals("one", firstMetaData.get("DROPPED"));
+ assertEquals("1.5", firstMetaData.get("RETYPED"), "a STRING column stores the value verbatim");
+
+ // Drop one column, retype another, add a third, and redeploy the same channel.
+ List columns = channel.getProperties().getMetaDataColumns();
+ assertTrue(columns.removeIf(column -> "DROPPED".equals(column.getName())),
+ "Channel fixture has no DROPPED column");
+ column(columns, "RETYPED").setType(MetaDataColumnType.NUMBER);
+ columns.add(new MetaDataColumn("ADDED", MetaDataColumnType.BOOLEAN, "added"));
+ server.deployChannel(channel, CHANNEL);
+
+ long second = submit(server, "kept", "two", "dropped", "two", "retyped", "1.5", "added", "true");
+ Map secondMetaData = metaData(second);
+ assertEquals("two", secondMetaData.get("KEPT"));
+ assertEquals(true, secondMetaData.get("ADDED"), "the column added on redeploy stores a BOOLEAN");
+ assertFalse(secondMetaData.containsKey("DROPPED"),
+ () -> "DROPPED was removed from the channel but is still a column: " + secondMetaData.keySet());
+ assertEquals(0, new BigDecimal("1.5").compareTo((BigDecimal) secondMetaData.get("RETYPED")),
+ "the retyped column now stores a NUMBER");
+
+ // The table survived both ALTER TABLEs, so the message sent before the redeploy is still
+ // readable - minus the column that was dropped, and minus the value in the column that was
+ // retyped, which a type change drops and re-adds.
+ Map firstAfterRedeploy = metaData(first);
+ assertEquals("one", firstAfterRedeploy.get("KEPT"), "an untouched column kept its stored value");
+ assertFalse(firstAfterRedeploy.containsKey("DROPPED"));
+ assertNull(firstAfterRedeploy.get("RETYPED"), "a retyped column is dropped and re-added, losing old values");
+ }
+
+ /** Submits a message whose source map is the given key/value pairs. */
+ private long submit(OieServer server, String... sourceMapEntries) throws Exception {
+ Map sourceMap = new LinkedHashMap<>();
+ for (int i = 0; i < sourceMapEntries.length; i += 2) {
+ sourceMap.put(sourceMapEntries[i], sourceMapEntries[i + 1]);
+ }
+ return server.submitMessage(channelId, "Hello world!", sourceMap);
+ }
+
+ /** Reads one message's source custom metadata back off the server. */
+ private Map metaData(long messageId) throws Exception {
+ return Harness.awaitConnectorStatus(channelId, messageId, 0, Status.TRANSFORMED).getMetaDataMap();
+ }
+
+ private static MetaDataColumn column(List columns, String name) {
+ return columns.stream().filter(column -> name.equals(column.getName())).findFirst()
+ .orElseThrow(() -> new AssertionError("Channel fixture has no " + name + " column"));
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueRotationTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueRotationTest.java
new file mode 100644
index 0000000000..635dc0aeb8
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueRotationTest.java
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.donkey.model.message.ConnectorMessage;
+import com.mirth.connect.donkey.model.message.Status;
+
+/**
+ * A rotating destination queue steps over the message at its head rather than retrying it
+ * forever, so one message that never sends does not hold up the ones behind it.
+ *
+ * The first message submitted is the one the destination never accepts, and it therefore
+ * owns the head of the queue for the whole test. Without rotation the queue thread would keep
+ * re-acquiring it and nothing else would ever be sent, so every later message reaching SENT is
+ * the rotation, not just a slow queue.
+ *
+ *
The order they are sent in is deliberately not asserted. Rotation moves the queue's starting
+ * point past whatever it just returned, so a message can come round on a later cycle than the one
+ * that was queued after it - a run of this has produced 2, 3, 4, 1. Reordering is what rotation
+ * is; the claim is that each message is sent exactly once.
+ *
+ *
That the stuck message is still queued is read once, after the others are all sent: by then
+ * the queue thread has demonstrably been past it, so nothing has to be inferred from a wait.
+ */
+@DisplayName("220-queueing/03-destination-queue-rotation")
+@TestInstance(PER_CLASS)
+class DestinationQueueRotationTest {
+
+ private static final String CHANNEL = "channels/destination-queue-rotate.xml";
+
+ /** The configuration property the destination leaves messages queued on until it holds this run's token. */
+ private static final String GATE = "oie.smoketest.releaseDestinationQueueRotate";
+
+ private static final int DESTINATION = 1;
+
+ /** What the destination prefixes its handover position with. */
+ private static final String POSITION = "order=";
+
+ /** Messages behind the stuck one, which all send once the gate is open. */
+ private static final int SENDABLE_COUNT = 4;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void aMessageThatNeverSendsDoesNotBlockTheOnesQueuedBehindIt() throws Exception {
+ // The payload carries this run's token, so a gate left open by an earlier run cannot
+ // release this one. The first message is the stuck one, and being first it is also the
+ // one the queue would otherwise acquire over and over.
+ String token = UUID.randomUUID().toString();
+ long stuckMessageId;
+ List sendableMessageIds = new ArrayList<>();
+
+ try {
+ stuckMessageId = SharedServer.get().submitMessage(channelId, token + "#stuck", new LinkedHashMap<>());
+ for (int i = 1; i <= SENDABLE_COUNT; i++) {
+ sendableMessageIds.add(
+ SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>()));
+ }
+
+ Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, SENDABLE_COUNT + 1);
+ } finally {
+ // Also on failure, so the gate is never left holding an earlier run's token.
+ Harness.setConfigurationProperty(GATE, token);
+ }
+
+ List positions = new ArrayList<>();
+ for (long messageId : sendableMessageIds) {
+ ConnectorMessage sent = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT);
+ String stamp = MessageAssertions.responsePayload(sent.getResponse().getContent());
+ assertTrue(stamp != null && stamp.startsWith(POSITION),
+ "message " + messageId + " has an unexpected destination response: " + stamp);
+ positions.add(Integer.valueOf(stamp.substring(POSITION.length())));
+ }
+
+ // Sorted, because which cycle a message comes round on is rotation's business. That the
+ // positions are distinct and leave no gap is the claim: four messages, four handovers.
+ Collections.sort(positions);
+ List distinctPositions = new ArrayList<>();
+ for (int i = 1; i <= SENDABLE_COUNT; i++) {
+ distinctPositions.add(i);
+ }
+ assertEquals(distinctPositions, positions,
+ "rotation should step over the stuck message and send each of the others exactly once");
+
+ // Every message behind it has been sent, so the queue thread has acquired and returned the
+ // stuck one at least that many times. It is still queued: rotation moved past it rather
+ // than failing it or dropping it.
+ assertEquals(Status.QUEUED, Harness.connectorMessage(channelId, stuckMessageId, DESTINATION).getStatus(),
+ "rotation should leave the stuck message on the queue, not end it");
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueTest.java
new file mode 100644
index 0000000000..130e7dba2e
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueTest.java
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.donkey.model.message.ConnectorMessage;
+import com.mirth.connect.donkey.model.message.Status;
+
+/**
+ * A destination queue outlives the channel that owns it: messages a failing destination could
+ * not send are still queued after the channel is stopped and started, and the queue thread
+ * drains them in message order once the destination accepts them.
+ *
+ * The destination's in-memory buffer is set to 3 and the test queues eight messages, so the
+ * queue the restarted channel drains cannot be the buffer it filled before the stop - it has to
+ * be reloaded from the database and paged through.
+ *
+ *
The gate stays shut across the whole stop and start, so every step here is a positive
+ * signal the server reports - queue depth, deployed state, connector status - and nothing is
+ * inferred from time passing. That a stopped channel does not send at all is a channel
+ * lifecycle claim rather than a queueing one, and is not asserted here.
+ */
+@DisplayName("220-queueing/02-destination-queue")
+@TestInstance(PER_CLASS)
+class DestinationQueueTest {
+
+ private static final String CHANNEL = "channels/destination-queue.xml";
+
+ /** The configuration property the destination fails on until it holds this run's token. */
+ private static final String GATE = "oie.smoketest.releaseDestinationQueue";
+
+ private static final int DESTINATION = 1;
+
+ private static final int MESSAGE_COUNT = 8;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void queuedMessagesSurviveAChannelRestartAndDrainInOrder() throws Exception {
+ // The payload carries this run's token, so a gate left open by an earlier run cannot
+ // release this one, and an index, so a failure names the message that came out of order.
+ String token = UUID.randomUUID().toString();
+ List messageIds = new ArrayList<>();
+
+ try {
+ for (int i = 1; i <= MESSAGE_COUNT; i++) {
+ messageIds.add(SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>()));
+ }
+ // The destination leaves every message on the queue while the gate is shut, so the
+ // queue is at its full depth - past the buffer it holds in memory - before the stop.
+ Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT);
+
+ // Take the channel down and back up with the queue full and the gate still shut, so
+ // the queue the restarted channel drains is the one it reloaded from the database.
+ Harness.stopChannel(channelId);
+ Harness.startChannel(channelId);
+
+ Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT);
+ } finally {
+ // Also on failure, so a channel left behind by this test is not a wedged one.
+ Harness.setConfigurationProperty(GATE, token);
+ }
+
+ List observedOrder = new ArrayList<>();
+ for (long messageId : messageIds) {
+ ConnectorMessage sent = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT);
+ observedOrder.add(MessageAssertions.responsePayload(sent.getResponse().getContent()));
+ }
+
+ // The destination stamps each message with the position it was drained in, so the messages
+ // queued first must carry the lowest positions.
+ List queueOrder = new ArrayList<>();
+ for (int i = 1; i <= MESSAGE_COUNT; i++) {
+ queueOrder.add("order=" + i);
+ }
+ assertEquals(queueOrder, observedOrder, "the destination queue should drain in message order");
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/EncryptionAtRestTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/EncryptionAtRestTest.java
new file mode 100644
index 0000000000..6dc4b5e655
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/EncryptionAtRestTest.java
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.donkey.model.message.ConnectorMessage;
+import com.mirth.connect.donkey.model.message.Message;
+import com.mirth.connect.donkey.model.message.MessageContent;
+import com.mirth.connect.donkey.model.message.Status;
+
+/**
+ * A channel with {@code encryptData} set stores its message content encrypted. No fixture can
+ * assert this: encryption is per-message randomised, so the stored value is different every run,
+ * and the only fixed part of it is the header the ciphertext carries.
+ *
+ * Which of the two read APIs is used decides what comes back. {@code getMessages} with content
+ * leaves the DAO's decryption off, so it hands back the column as stored; {@code getMessageContent}
+ * decrypts. The two together are what makes "encrypted at rest" observable from a client.
+ */
+@DisplayName("210-encryption-at-rest")
+@TestInstance(PER_CLASS)
+class EncryptionAtRestTest {
+
+ private static final String CHANNEL = "channels/encryption-at-rest.xml";
+
+ /** Prefix {@code KeyEncryptor} writes in front of every ciphertext, carrying the IV. */
+ private static final String ENCRYPTION_HEADER = "{alg=";
+
+ private static final String RAW = "Hello world!";
+ /** The source transformer prefixes the message, so transformed and encoded differ from raw. */
+ private static final String TRANSFORMED = "transformed:" + RAW;
+
+ private static final int SOURCE = 0;
+ private static final int DESTINATION = 1;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void storesEveryContentStageEncrypted() throws Exception {
+ Message stored = process();
+ ConnectorMessage source = connector(stored, SOURCE);
+ ConnectorMessage destination = connector(stored, DESTINATION);
+
+ assertStoredEncrypted("source raw", source.getRaw());
+ assertStoredEncrypted("source transformed", source.getTransformed());
+ assertStoredEncrypted("source encoded", source.getEncoded());
+ // The destination's own content, written by a different DAO call than the source's.
+ assertStoredEncrypted("destination sent", destination.getSent());
+ }
+
+ @Test
+ void decryptsContentWhenReadBack() throws Exception {
+ Message stored = process();
+ ConnectorMessage source = connector(
+ SharedServer.get().fetchDecryptedMessage(channelId, stored.getMessageId(),
+ List.of(SOURCE, DESTINATION)),
+ SOURCE);
+
+ assertDecrypted("source raw", source.getRaw(), RAW);
+ assertDecrypted("source transformed", source.getTransformed(), TRANSFORMED);
+ assertDecrypted("source encoded", source.getEncoded(), TRANSFORMED);
+ }
+
+ /** Submits one message and returns it as stored, once the destination has finished with it. */
+ private Message process() throws Exception {
+ OieServer server = SharedServer.get();
+ long messageId = server.submitMessage(channelId, RAW, new LinkedHashMap<>());
+ Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT);
+ return server.fetchMessage(channelId, messageId);
+ }
+
+ /**
+ * Asserts that what the server stored is ciphertext: flagged encrypted, carrying the
+ * encryptor's header, and not the plaintext it was made from.
+ */
+ private static void assertStoredEncrypted(String label, MessageContent content) {
+ assertNotNull(content, () -> label + " was not stored at all");
+ String stored = content.getContent();
+ assertTrue(content.isEncrypted(), () -> label + " is not flagged as encrypted: " + stored);
+ assertTrue(stored.startsWith(ENCRYPTION_HEADER),
+ () -> label + " does not carry an encryption header: " + stored);
+ assertFalse(stored.contains(RAW), () -> label + " leaks the plaintext payload: " + stored);
+ }
+
+ /** Asserts that the decrypting read path recovers the original content. */
+ private static void assertDecrypted(String label, MessageContent content, String expected) {
+ assertNotNull(content, () -> label + " was not returned by the decrypting read");
+ assertEquals(expected, content.getContent(), () -> label + " did not decrypt to its original");
+ assertFalse(content.isEncrypted(), () -> label + " is still flagged as encrypted after decryption");
+ }
+
+ private static ConnectorMessage connector(Message message, int metaDataId) {
+ assertNotNull(message, "The server returned no message");
+ ConnectorMessage connectorMessage = message.getConnectorMessages().get(metaDataId);
+ assertNotNull(connectorMessage,
+ () -> "Message " + message.getMessageId() + " has no connector " + metaDataId
+ + "; present ids: " + message.getConnectorMessages().keySet());
+ return connectorMessage;
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java
index 5a2b702819..513b50fdef 100644
--- a/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java
@@ -6,7 +6,9 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
@@ -18,6 +20,7 @@
import com.mirth.connect.donkey.model.message.Message;
import com.mirth.connect.donkey.model.message.MessageContent;
import com.mirth.connect.donkey.model.message.Status;
+import com.mirth.connect.donkey.model.message.attachment.Attachment;
/**
* The entry points the generated smoke tests call (see :smoketest:generateSmokeTests
@@ -28,6 +31,16 @@ public final class Harness {
/** Statuses that mean the server has not finished with the message yet. */
private static final List PENDING_STATUSES = List.of(Status.PENDING, Status.QUEUED);
+ /**
+ * How long to keep retrying after the message looks terminal. Some work outruns the
+ * statuses: a channel with a queued destination and removeContentOnCompletion deletes
+ * its content in a transaction committed after the destination is already SENT.
+ */
+ private static final Duration TERMINAL_GRACE = Duration.ofSeconds(5);
+
+ /** How often to re-read a message while waiting for it to reach a state. */
+ private static final Duration POLL_INTERVAL = Duration.ofMillis(250);
+
private Harness() {
}
@@ -63,9 +76,9 @@ public static void undeploy(String channelId) {
/**
* Submits {@code /source} (with {@code /source_sourcemap.yml} when
* {@code hasSourceMap}) into the channel, then retries the named assertion files until
- * they all hold or the message reaches a terminal state. Because the message is written
- * asynchronously, an early poll can legitimately fail; only a failure that persists once
- * the message is terminal is a real failure.
+ * they all hold or the message has been terminal for {@link #TERMINAL_GRACE}. Because the
+ * message is written asynchronously, an early poll can legitimately fail; only a failure
+ * that outlives the message's terminal state is a real failure.
*/
public static void runMessage(String channelId, String base, boolean hasSourceMap, String... assertionFiles)
throws Exception {
@@ -76,8 +89,11 @@ public static void runMessage(String channelId, String base, boolean hasSourceMa
// Load the fixtures once; the poll loop below may check them many times.
Map assertions = new LinkedHashMap<>();
+ // Attachments are a second read against the server, so only pay for it when asked.
+ boolean fetchAttachments = false;
for (String fileName : assertionFiles) {
- assertions.put(fileName, resource(base + "/" + fileName));
+ assertions.put(fileName, resource(base + "/" + fileName, MessageAssertions.charsetFor(fileName)));
+ fetchAttachments |= MessageAssertions.isAttachmentFixture(fileName);
}
long messageId = server().submitMessage(channelId, source, sourceMap);
@@ -85,40 +101,192 @@ public static void runMessage(String channelId, String base, boolean hasSourceMa
long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos();
AssertionError lastFailure = null;
Message lastMessage = null;
+ List lastAttachments = List.of();
+ long graceDeadline = 0;
while (System.nanoTime() < deadline) {
Message message = server().fetchMessage(channelId, messageId);
if (message != null) {
lastMessage = message;
+ List attachments = fetchAttachments
+ ? server().fetchAttachments(channelId, messageId)
+ : List.of();
+ lastAttachments = attachments;
try {
for (Map.Entry assertion : assertions.entrySet()) {
- MessageAssertions.assertFixtureFile(message, assertion.getKey(), assertion.getValue());
+ MessageAssertions.assertFixtureFile(message, attachments, assertion.getKey(),
+ assertion.getValue());
}
return;
} catch (AssertionError e) {
lastFailure = e;
if (isTerminal(message)) {
- break;
+ if (graceDeadline == 0) {
+ graceDeadline = System.nanoTime() + TERMINAL_GRACE.toNanos();
+ } else if (System.nanoTime() >= graceDeadline) {
+ break;
+ }
}
}
}
- Thread.sleep(500);
+ Thread.sleep(POLL_INTERVAL.toMillis());
}
if (lastFailure != null) {
throw new AssertionError(base + " failed: " + lastFailure.getMessage()
- + "\n\n" + describe(lastMessage), lastFailure);
+ + "\n\n" + describe(lastMessage, lastAttachments), lastFailure);
}
throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for message "
- + messageId + " for fixture " + base + "\n\n" + describe(lastMessage));
+ + messageId + " for fixture " + base + "\n\n" + describe(lastMessage, lastAttachments));
+ }
+
+ /**
+ * Polls until one connector of one message reaches {@code status}, and returns it. Java
+ * tests need this where {@link #runMessage} cannot help: it stops at the first terminal
+ * state, so it can never observe a message mid-flight.
+ */
+ public static ConnectorMessage awaitConnectorStatus(String channelId, long messageId, int metaDataId,
+ Status status) throws Exception {
+ long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos();
+ Status lastStatus = null;
+ do {
+ Message message = server().fetchMessage(channelId, messageId);
+ ConnectorMessage connectorMessage = message == null ? null
+ : message.getConnectorMessages().get(metaDataId);
+ if (connectorMessage != null) {
+ lastStatus = connectorMessage.getStatus();
+ if (lastStatus == status) {
+ return connectorMessage;
+ }
+ }
+ Thread.sleep(POLL_INTERVAL.toMillis());
+ } while (System.nanoTime() < deadline);
+
+ throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for connector "
+ + metaDataId + " of message " + messageId + " to reach " + status + "; last status was " + lastStatus);
+ }
+
+ /**
+ * Polls until the server has marked one message processed, and returns it. A connector reaching
+ * its final status is not the end of the message: the engine still has the postprocessor to run
+ * and the message row to mark, and that last commit is where the remaining statistics land. A
+ * test that reads anything channel-wide has to wait for it.
+ */
+ public static Message awaitProcessed(String channelId, long messageId) throws Exception {
+ long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos();
+ Message message = null;
+ do {
+ message = server().fetchMessage(channelId, messageId);
+ if (message != null && message.isProcessed()) {
+ return message;
+ }
+ Thread.sleep(POLL_INTERVAL.toMillis());
+ } while (System.nanoTime() < deadline);
+
+ throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for message "
+ + messageId + " of channel " + channelId + " to be processed\n\n" + describe(message, List.of()));
+ }
+
+ /**
+ * Polls until at least {@code minimum} messages are queued for one connector. Queue tests use
+ * this to know a queue has really built up - past its in-memory buffer, say - before releasing
+ * whatever is holding it, so that the depth is a precondition the test enforces rather than one
+ * it hopes for.
+ */
+ public static void awaitQueueSizeAtLeast(String channelId, int metaDataId, long minimum) throws Exception {
+ long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos();
+ Long lastSize = null;
+ do {
+ lastSize = server().queueSize(channelId, metaDataId);
+ if (lastSize != null && lastSize >= minimum) {
+ return;
+ }
+ Thread.sleep(POLL_INTERVAL.toMillis());
+ } while (System.nanoTime() < deadline);
+
+ throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for connector "
+ + metaDataId + " of channel " + channelId + " to have at least " + minimum
+ + " messages queued; last size was " + lastSize);
}
- /** Reads a staged fixture from the classpath. */
+ /**
+ * Polls until a connector's queue has fallen to {@code maximum} messages or fewer. The mirror of
+ * {@link #awaitQueueSizeAtLeast}, for a test that emptied a queue rather than filled one.
+ */
+ public static void awaitQueueSizeAtMost(String channelId, int metaDataId, long maximum) throws Exception {
+ long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos();
+ Long lastSize = null;
+ do {
+ lastSize = server().queueSize(channelId, metaDataId);
+ if (lastSize != null && lastSize <= maximum) {
+ return;
+ }
+ Thread.sleep(POLL_INTERVAL.toMillis());
+ } while (System.nanoTime() < deadline);
+
+ throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for connector "
+ + metaDataId + " of channel " + channelId + " to have at most " + maximum
+ + " messages queued; last size was " + lastSize);
+ }
+
+ /**
+ * Reads one connector of one message as it stands right now, without waiting for anything.
+ * This is for asserting where a message has not got to, which is only sound once
+ * something else has proved the engine went past it - never on its own, as a message that has
+ * simply not been picked up yet looks identical.
+ */
+ public static ConnectorMessage connectorMessage(String channelId, long messageId, int metaDataId)
+ throws Exception {
+ Message message = server().fetchMessage(channelId, messageId);
+ ConnectorMessage connectorMessage = message == null ? null
+ : message.getConnectorMessages().get(metaDataId);
+ if (connectorMessage == null) {
+ throw new AssertionError("Message " + messageId + " of channel " + channelId
+ + " has no connector " + metaDataId);
+ }
+ return connectorMessage;
+ }
+
+ /** Stops a deployed channel, so its queue threads are no longer running. */
+ public static void stopChannel(String channelId) throws Exception {
+ server().stopChannel(channelId);
+ }
+
+ /** Halts a deployed channel, interrupting whatever it is processing rather than waiting. */
+ public static void haltChannel(String channelId) throws Exception {
+ server().haltChannel(channelId);
+ }
+
+ /** Starts a stopped channel back up. */
+ public static void startChannel(String channelId) throws Exception {
+ server().startChannel(channelId);
+ }
+
+ /** Undeploys a channel without removing it, leaving everything it stored in place. */
+ public static void undeployChannel(String channelId) throws Exception {
+ server().undeployChannel(channelId);
+ }
+
+ /** Sets one configuration map entry, which channel scripts read back as {@code configurationMap}. */
+ public static void setConfigurationProperty(String key, String value) throws Exception {
+ server().setConfigurationProperty(key, value);
+ }
+
+ /** Reads a staged fixture from the classpath as UTF-8 text. */
static String resource(String path) {
+ return resource(path, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Reads a staged fixture from the classpath. Most fixtures are text and are read as UTF-8;
+ * an attachment's content can be any bytes at all, so it is read as ISO-8859-1, which maps
+ * every byte to one char and back again and so compares byte for byte.
+ */
+ static String resource(String path, Charset charset) {
try (InputStream in = Harness.class.getClassLoader().getResourceAsStream(path)) {
if (in == null) {
throw new IllegalStateException("Missing fixture resource on the classpath: " + path);
}
- return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ return new String(in.readAllBytes(), charset);
} catch (IOException e) {
throw new UncheckedIOException("Could not read fixture resource " + path, e);
}
@@ -138,7 +306,7 @@ private static boolean isTerminal(Message message) {
}
/** Renders the message the way a fixture author needs to see it to fix a mismatch. */
- private static String describe(Message message) {
+ private static String describe(Message message, List attachments) {
if (message == null) {
return "No message was retrieved from the server.";
}
@@ -159,12 +327,19 @@ private static String describe(Message message) {
appendContent(detail, "encoded", connectorMessage.getEncoded());
appendContent(detail, "sent", connectorMessage.getSent());
appendContent(detail, "response", connectorMessage.getResponse());
+ appendContent(detail, "responseTransformed", connectorMessage.getResponseTransformed());
+ appendContent(detail, "processedResponse", connectorMessage.getProcessedResponse());
detail.append("\n connectorMap=").append(connectorMessage.getConnectorMap())
.append("\n metaDataMap=").append(connectorMessage.getMetaDataMap());
if (connectorMessage.getProcessingError() != null) {
detail.append("\n processingError=").append(connectorMessage.getProcessingError());
}
});
+ MessageAssertions.order(message, attachments).forEach(attachment -> detail
+ .append("\n attachment ").append(attachment.getId())
+ .append(" type=").append(attachment.getType())
+ .append(" content=")
+ .append(quote(new String(attachment.getContent(), StandardCharsets.ISO_8859_1))));
return detail.toString();
}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java
index 59fef4fe22..0de84fbae1 100644
--- a/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java
@@ -37,11 +37,17 @@ final class HarnessConfig {
Duration.ofSeconds(Long.parseLong(System.getProperty("oie.timeoutSeconds", "90")));
/**
- * Per-request socket timeout. {@code new Client(address)} defaults to an infinite
- * timeout, which would let a wedged server hang CI instead of failing it.
+ * Per-request socket timeout. {@code new Client(address)} defaults to an infinite timeout,
+ * which would let a wedged server hang CI instead of failing it.
+ *
+ * This has to clear the slowest thing one request can legitimately queue behind, not the
+ * time a request normally takes: test classes run in parallel against one server, and a
+ * channel deploy holds it long enough that an unrelated {@code getChannelStatus} can wait
+ * seconds. {@link #TIMEOUT} still bounds the test as a whole, so a genuinely wedged server
+ * fails - just not on the first read that was merely waiting its turn.
*/
static final int REQUEST_TIMEOUT_MILLIS =
- Integer.parseInt(System.getProperty("oie.requestTimeoutMillis", "15000"));
+ Integer.parseInt(System.getProperty("oie.requestTimeoutMillis", "30000"));
private HarnessConfig() {
}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java
index f9849435ae..062ce8421f 100644
--- a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java
@@ -3,7 +3,15 @@
package org.openintegrationengine.smoketest;
+import java.math.BigDecimal;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Comparator;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
@@ -16,6 +24,7 @@
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.donkey.model.message.attachment.Attachment;
import com.mirth.connect.model.converters.ObjectXMLSerializer;
/**
@@ -32,10 +41,27 @@ final class MessageAssertions {
/** Fixture wildcard: matches any run of characters, for timestamps and generated ids. */
private static final String ANY_WILDCARD = "((ANY))";
+ /**
+ * Fixture sentinel asserting that nothing is stored: a whole content file, or one key of a
+ * metadata file.
+ */
+ private static final String NONE_SENTINEL = "((NONE))";
+
private static final Pattern RESPONSE_ENVELOPE = Pattern.compile("^\\s*].*", Pattern.DOTALL);
+ /**
+ * Attachment assertion files are {@code attachment} plus an optional suffix, where
+ * {@code NN} is the attachment's position in the message rather than its id, which is a
+ * UUID the server generates.
+ */
+ private static final Pattern ATTACHMENT_NAME = Pattern.compile("attachment(\\d+)(_type)?");
+
+ /** The token an attachment handler leaves in the message where it took an attachment out. */
+ private static final Pattern ATTACHMENT_TOKEN = Pattern.compile("\\$\\{ATTACH:([^}]+)\\}");
+
/** Destination assertion files are {@code dest} plus an optional suffix. */
- private static final Pattern DEST_NAME = Pattern.compile("dest(\\d+)(_transformed|_response|_status|_metadata\\.yml)?");
+ private static final Pattern DEST_NAME = Pattern.compile(
+ "dest(\\d+)(_transformed|_response|_processed_response|_processing_error|_response_error|_status|_metadata\\.yml)?");
/** Source connector metadata id; destination N is metadata id N. */
private static final int SOURCE_META_DATA_ID = 0;
@@ -43,26 +69,108 @@ final class MessageAssertions {
private MessageAssertions() {
}
+ /**
+ * True for a fixture file that asserts an attachment rather than something on the message.
+ * Attachments are stored in their own table, so the harness has to ask the server for them
+ * separately, and only does so when a fixture names one.
+ */
+ static boolean isAttachmentFixture(String fileName) {
+ return ATTACHMENT_NAME.matcher(fileName).matches();
+ }
+
+ /**
+ * The charset a fixture file is read with. An attachment's content is arbitrary bytes - an
+ * image, a DICOM object - so it is read as ISO-8859-1, which maps every byte to one char and
+ * back without loss, making the comparison a byte-for-byte one. Everything else is text.
+ */
+ static Charset charsetFor(String fileName) {
+ Matcher matcher = ATTACHMENT_NAME.matcher(fileName);
+ return matcher.matches() && matcher.group(2) == null ? StandardCharsets.ISO_8859_1
+ : StandardCharsets.UTF_8;
+ }
+
/**
* Applies one fixture file's assertion to the message.
*
- * @param fileName the fixture file name, e.g. {@code source_status} or {@code dest01}
- * @param content that file's text, already loaded from the classpath
+ * @param attachments the message's attachments, empty unless a fixture asked for them
+ * @param fileName the fixture file name, e.g. {@code source_status} or {@code dest01}
+ * @param content that file's text, already loaded from the classpath
*/
- static void assertFixtureFile(Message message, String fileName, String content) {
+ static void assertFixtureFile(Message message, List attachments, String fileName, String content) {
+ if (isAttachmentFixture(fileName)) {
+ assertAttachment(message, attachments, fileName, content);
+ return;
+ }
switch (fileName) {
case "source_status" -> assertStatus("source status", content,
connector(message, SOURCE_META_DATA_ID, fileName).getStatus());
+ case "source_raw" -> assertContent("source raw", content,
+ content(connector(message, SOURCE_META_DATA_ID, fileName).getRaw()));
case "source_transformed" -> assertContent("source transformed", content,
content(connector(message, SOURCE_META_DATA_ID, fileName).getTransformed()));
+ case "source_encoded" -> assertContent("source encoded", content,
+ content(connector(message, SOURCE_META_DATA_ID, fileName).getEncoded()));
+ case "source_processing_error" -> assertContent("source processing error", content,
+ connector(message, SOURCE_META_DATA_ID, fileName).getProcessingError());
case "source_response" -> assertResponse("source response", content,
- connector(message, SOURCE_META_DATA_ID, fileName));
+ connector(message, SOURCE_META_DATA_ID, fileName).getResponse());
case "source_metadata.yml" -> assertMetadata("source_metadata.yml", parseYamlMap(content),
connector(message, SOURCE_META_DATA_ID, fileName));
default -> assertDestination(message, fileName, content);
}
}
+ /**
+ * Asserts one attachment's content or mime type. {@code attachment01} is the attachment whose
+ * token appears first in the source raw content, not the first the server hands back: that
+ * list is ordered by id, which is a generated UUID and so bears no relation to the message.
+ * An attachment the message does not reference sorts after the ones it does, by id, so a
+ * handler that stores attachments without leaving tokens behind still has a stable order.
+ */
+ private static void assertAttachment(Message message, List attachments, String fileName,
+ String content) {
+ Matcher matcher = ATTACHMENT_NAME.matcher(fileName);
+ if (!matcher.matches()) {
+ throw new IllegalStateException("Unrecognised fixture file name: " + fileName);
+ }
+ int position = Integer.parseInt(matcher.group(1));
+ List ordered = order(message, attachments);
+ Attachment attachment = position >= 1 && position <= ordered.size() ? ordered.get(position - 1) : null;
+
+ if ("_type".equals(matcher.group(2))) {
+ assertMatches(fileName, content.trim(), attachment == null ? null : attachment.getType());
+ } else {
+ assertMatches(fileName, content, attachment == null ? null
+ : new String(attachment.getContent(), StandardCharsets.ISO_8859_1));
+ }
+ }
+
+ /**
+ * Puts a message's attachments into the order a fixture numbers them by: the order their
+ * tokens appear in the source raw content, then whatever is left over, by id.
+ */
+ static List order(Message message, List attachments) {
+ Map byId = new LinkedHashMap<>();
+ attachments.forEach(attachment -> byId.put(attachment.getId(), attachment));
+
+ List ordered = new ArrayList<>();
+ Map connectorMessages = message.getConnectorMessages();
+ ConnectorMessage source = connectorMessages == null ? null : connectorMessages.get(SOURCE_META_DATA_ID);
+ String raw = source == null ? null : content(source.getRaw());
+ if (raw != null) {
+ Matcher tokens = ATTACHMENT_TOKEN.matcher(raw);
+ while (tokens.find()) {
+ Attachment referenced = byId.remove(tokens.group(1));
+ if (referenced != null) {
+ ordered.add(referenced);
+ }
+ }
+ }
+
+ byId.values().stream().sorted(Comparator.comparing(Attachment::getId)).forEach(ordered::add);
+ return ordered;
+ }
+
private static void assertDestination(Message message, String fileName, String content) {
Matcher matcher = DEST_NAME.matcher(fileName);
if (!matcher.matches()) {
@@ -76,7 +184,10 @@ private static void assertDestination(Message message, String fileName, String c
switch (suffix) {
case "" -> assertContent(fileName, content, content(destination.getSent()));
case "_transformed" -> assertContent(fileName, content, content(destination.getTransformed()));
- case "_response" -> assertResponse(fileName, content, destination);
+ case "_response" -> assertResponse(fileName, content, destination.getResponse());
+ case "_processed_response" -> assertResponse(fileName, content, destination.getProcessedResponse());
+ case "_processing_error" -> assertContent(fileName, content, destination.getProcessingError());
+ case "_response_error" -> assertContent(fileName, content, destination.getResponseError());
case "_status" -> assertStatus(fileName, content, destination.getStatus());
case "_metadata.yml" -> assertMetadata(fileName, parseYamlMap(content), destination);
default -> throw new IllegalStateException("Unhandled fixture suffix: " + suffix);
@@ -119,15 +230,18 @@ private static void assertContent(String label, String expected, String actual)
* fixture actually describes. Line endings are normalised because HL7 acknowledgements
* come back CR-delimited while the fixture files are LF-delimited.
*/
- private static void assertResponse(String label, String expected, ConnectorMessage connectorMessage) {
- String stored = content(connectorMessage.getResponse());
- String actual = stored;
- if (stored != null && RESPONSE_ENVELOPE.matcher(stored).matches()) {
- Response response = ObjectXMLSerializer.getInstance().deserialize(stored.trim(), Response.class);
- String payload = response == null ? null : response.getMessage();
- actual = payload == null ? null : payload.replace("\r\n", "\n").replace('\r', '\n');
+ private static void assertResponse(String label, String expected, MessageContent responseContent) {
+ assertMatches(label, expected, responsePayload(content(responseContent)));
+ }
+
+ /** Unwraps a stored {@link Response} to the payload a fixture describes, or passes it through. */
+ static String responsePayload(String stored) {
+ if (stored == null || !RESPONSE_ENVELOPE.matcher(stored).matches()) {
+ return stored;
}
- assertMatches(label, expected, actual);
+ Response response = ObjectXMLSerializer.getInstance().deserialize(stored.trim(), Response.class);
+ String payload = response == null ? null : response.getMessage();
+ return payload == null ? null : payload.replace("\r\n", "\n").replace('\r', '\n');
}
/**
@@ -153,13 +267,23 @@ private static void assertSubset(String label, String path, Map
Map actual) {
for (Map.Entry entry : expected.entrySet()) {
String keyPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
+ Object expectedValue = entry.getValue();
+ Object actualValue = actual.get(entry.getKey());
+
+ // A key whose value is the sentinel asserts the opposite: nothing was stored for it.
+ if (NONE_SENTINEL.equals(expectedValue)) {
+ if (actualValue != null) {
+ throw new AssertionError("Metadata mismatch for " + label + " at " + keyPath
+ + ": expected no value, found " + describe(actualValue));
+ }
+ continue;
+ }
+
if (!actual.containsKey(entry.getKey())) {
throw new AssertionError("Metadata mismatch for " + label + ": missing key " + keyPath
+ "; present keys: " + actual.keySet());
}
- Object expectedValue = entry.getValue();
- Object actualValue = actual.get(entry.getKey());
if (expectedValue instanceof Map, ?> expectedMap) {
if (!(actualValue instanceof Map, ?> actualMap)) {
throw new AssertionError("Metadata mismatch for " + label + " at " + keyPath
@@ -181,11 +305,41 @@ private static boolean scalarsEqual(Object expected, Object actual) {
if (expected == null || actual == null) {
return Objects.equals(expected, actual);
}
- return String.valueOf(expected).equals(String.valueOf(actual));
+ return scalarText(expected).equals(scalarText(actual));
+ }
+
+ /**
+ * Renders a scalar the way a fixture writes it. A TIMESTAMP custom metadata column comes back
+ * as a {@link Calendar}, whose {@code toString} spells out every field and the JVM's time zone,
+ * so it is rendered as its UTC instant instead: a fixture writes {@code 2010-01-02T13:01:02Z}.
+ *
+ * A NUMBER column comes back as a {@link BigDecimal} whose scale is the dialect's business:
+ * the column is {@code DECIMAL(31, 15)} everywhere, and derby, postgres, mysql and sqlserver
+ * all return the column's scale, so {@code 1234.5678} arrives as {@code 1234.567800000000000},
+ * while oracle returns the scale the value was stored with. Trailing zeros are therefore
+ * dropped, which leaves a fixture free to write the value it sent.
+ */
+ private static String scalarText(Object value) {
+ if (value instanceof Calendar calendar) {
+ return Instant.ofEpochMilli(calendar.getTimeInMillis()).toString();
+ }
+ if (value instanceof BigDecimal number) {
+ return number.stripTrailingZeros().toPlainString();
+ }
+ return String.valueOf(value);
}
- /** Compares an assertion file to actual content, honouring {@value #ANY_WILDCARD}. */
+ /**
+ * Compares an assertion file to actual content, honouring {@value #ANY_WILDCARD} and
+ * {@value #NONE_SENTINEL}.
+ */
private static void assertMatches(String label, String expected, String actual) {
+ if (NONE_SENTINEL.equals(expected.trim())) {
+ if (actual != null) {
+ throw new AssertionError("Expected " + label + " content to be absent, found " + describe(actual));
+ }
+ return;
+ }
if (actual == null) {
throw new AssertionError("Expected " + label + " content but the server stored none");
}
@@ -222,7 +376,7 @@ private static String content(MessageContent messageContent) {
}
private static String describe(Object value) {
- return value == null ? "" : "\"" + value + "\"";
+ return value == null ? "" : "\"" + scalarText(value) + "\"";
}
@SuppressWarnings("unchecked")
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageDeletionTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageDeletionTest.java
new file mode 100644
index 0000000000..b55c57df71
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageDeletionTest.java
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import com.mirth.connect.donkey.model.message.ConnectorMessage;
+import com.mirth.connect.donkey.model.message.Message;
+import com.mirth.connect.donkey.model.message.Status;
+import com.mirth.connect.donkey.model.message.attachment.Attachment;
+import com.mirth.connect.model.ChannelStatistics;
+
+/**
+ * Message pruning and deletion over the client API: the three deletes the message browser offers,
+ * and what each one takes with it. There is no fixture shape for this - a fixture submits a message
+ * and asserts what was stored, where every assertion here is about what is no longer stored after a
+ * later call.
+ *
+ * The channel carries an attachment per message, a custom metadata column, and two destinations,
+ * so every table a delete has to cascade into (content, attachments, custom metadata, connector
+ * messages) holds a row before the delete runs. The cascades themselves are the point: they are
+ * separate statements per dialect, and a database that keeps real foreign keys refuses the parent
+ * delete if a child row is left behind, so a delete that returns at all has already proved most of
+ * its own cascade.
+ */
+@DisplayName("240-message-deletion/01-cascades")
+@TestInstance(PER_CLASS)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class MessageDeletionTest {
+
+ private static final String CHANNEL = "channels/message-deletion.xml";
+
+ /** Two regex attachments and a custom metadata value, so a delete has something to cascade into. */
+ private static final String PAYLOAD = "PHOTO:snapshot bytes;REPORT:written up;body";
+
+ /** What {@link #PAYLOAD} looks like once stored: the attachment handler has taken both values out. */
+ private static final Pattern STORED_PAYLOAD = Pattern
+ .compile("PHOTO:\\$\\{ATTACH:[0-9a-f-]+\\};REPORT:\\$\\{ATTACH:[0-9a-f-]+\\};body");
+
+ /** The last destination in the chain; once it is SENT the message is done. */
+ private static final int LAST_DESTINATION = 2;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("deleting one message takes its content, metadata and attachments with it")
+ void deleteOneMessage() throws Exception {
+ OieServer server = SharedServer.get();
+ long deleted = submit("deleted");
+ long kept = submit("kept");
+
+ assertEquals(2, attachments(deleted).size(), "the channel should have stored both attachments");
+
+ server.removeMessage(channelId, deleted, null);
+
+ assertNull(server.fetchMessage(channelId, deleted), "the message row survived the delete");
+ assertEquals(List.of(), attachments(deleted), "the message's attachments outlived the message");
+
+ // The delete was scoped to one message id, not to the channel.
+ Message survivor = server.fetchMessage(channelId, kept);
+ assertNotNull(survivor, "deleting one message deleted another one too");
+ assertEquals("kept", survivor.getConnectorMessages().get(0).getMetaDataMap().get("MDSTRING"));
+ assertEquals(2, attachments(kept).size(), "deleting one message took another message's attachments");
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("deleting one connector message leaves the rest of the message")
+ void deleteConnectorMessage() throws Exception {
+ OieServer server = SharedServer.get();
+ long messageId = submit("partial");
+
+ server.removeMessage(channelId, messageId, LAST_DESTINATION);
+
+ Message message = server.fetchMessage(channelId, messageId);
+ assertNotNull(message, "deleting a destination's connector message deleted the whole message");
+ Map connectors = message.getConnectorMessages();
+ assertFalse(connectors.containsKey(LAST_DESTINATION),
+ () -> "destination " + LAST_DESTINATION + " is still there: " + connectors.keySet());
+ assertTrue(connectors.containsKey(0) && connectors.containsKey(1),
+ () -> "the source and the other destination should be untouched, but got " + connectors.keySet());
+
+ // The remaining connectors kept their own rows in every table the cascade touched.
+ ConnectorMessage source = connectors.get(0);
+ assertStoredPayload(source.getRaw().getContent(), "the source's content went with the destination's");
+ assertEquals("partial", source.getMetaDataMap().get("MDSTRING"),
+ "the source's custom metadata went with the destination's");
+ assertNotNull(connectors.get(1).getEncoded(), "destination 1's content went with destination 2's");
+ assertEquals(2, attachments(messageId).size(),
+ "attachments belong to the message, not to one connector, so a connector delete keeps them");
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("removing all messages empties the channel and leaves it usable")
+ void removeAllMessages() throws Exception {
+ OieServer server = SharedServer.get();
+ long messageId = submit("bulk");
+ assertTrue(server.messageCount(channelId) > 0, "nothing to remove");
+ ChannelStatistics before = server.statistics(channelId);
+
+ server.removeAllMessages(channelId, false);
+
+ assertEquals(0, server.messageCount(channelId), "the channel still holds messages");
+ assertEquals(List.of(), attachments(messageId), "the attachment table was not truncated with the messages");
+
+ // Without clearStatistics the counters are a lifetime total and survive the delete.
+ assertStatistics(before, server.statistics(channelId),
+ "removing messages should not have touched the statistics");
+
+ // Emptying the channel drops and re-adds the foreign keys between its message tables; a
+ // message that processes and reads back afterwards is what proves they were all restored.
+ long after = submit("after-bulk-delete");
+ Message message = server.fetchMessage(channelId, after);
+ assertNotNull(message, "the channel stored nothing after all its messages were removed");
+ assertStoredPayload(message.getConnectorMessages().get(0).getRaw().getContent(),
+ "the message stored after the bulk delete did not come back intact");
+ assertEquals(2, attachments(after).size());
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("removing all messages can reset the channel's statistics at the same time")
+ void removeAllMessagesClearingStatistics() throws Exception {
+ OieServer server = SharedServer.get();
+ submit("statistics");
+ ChannelStatistics before = server.statistics(channelId);
+ assertTrue(before.getReceived() > 0 && before.getSent() > 0,
+ () -> "the channel should have counted the messages it processed, but got " + before);
+
+ server.removeAllMessages(channelId, true);
+
+ assertEquals(0, server.messageCount(channelId));
+ assertStatistics(new ChannelStatistics(), server.statistics(channelId),
+ "clearing the statistics should have zeroed every counter");
+ }
+
+ /** Submits one message whose {@code MDSTRING} column holds {@code label}, and waits for it to finish. */
+ private long submit(String label) throws Exception {
+ Map sourceMap = new LinkedHashMap<>();
+ sourceMap.put("mdstring", label);
+ long messageId = SharedServer.get().submitMessage(channelId, PAYLOAD, sourceMap);
+ Harness.awaitConnectorStatus(channelId, messageId, LAST_DESTINATION, Status.SENT);
+ // The statistics assertions read a channel-wide total, so wait for the commit that
+ // finishes the message rather than only for its last destination.
+ Harness.awaitProcessed(channelId, messageId);
+ return messageId;
+ }
+
+ /** Asserts one stored raw payload is the whole message, with an attachment token per extracted value. */
+ private static void assertStoredPayload(String raw, String message) {
+ assertTrue(raw != null && STORED_PAYLOAD.matcher(raw).matches(), () -> message + "; got " + raw);
+ }
+
+ private List attachments(long messageId) throws Exception {
+ return SharedServer.get().fetchAttachments(channelId, messageId);
+ }
+
+ /** Compares the four lifetime counters, which is what a delete can change. */
+ private static void assertStatistics(ChannelStatistics expected, ChannelStatistics actual, String message) {
+ assertEquals(
+ List.of(expected.getReceived(), expected.getFiltered(), expected.getSent(), expected.getError()),
+ List.of(actual.getReceived(), actual.getFiltered(), actual.getSent(), actual.getError()),
+ () -> message + "; expected " + expected + " but got " + actual);
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageStorageDisabledTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageStorageDisabledTest.java
new file mode 100644
index 0000000000..b85d86d151
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageStorageDisabledTest.java
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.LinkedHashMap;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.donkey.model.message.Message;
+
+/**
+ * {@code MessageStorageMode.DISABLED}, the one storage level that ci/tests/160-message-storage-levels
+ * cannot express. The channel is given a pass-through DAO, so no message row is ever written and
+ * there is no message for the fixture harness to retrieve and assert against.
+ */
+@DisplayName("160-message-storage-levels/05-disabled")
+@TestInstance(PER_CLASS)
+class MessageStorageDisabledTest {
+
+ private static final String CHANNEL = "channels/message-storage-disabled.xml";
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void storesNoMessage() throws Exception {
+ OieServer server = SharedServer.get();
+ long messageId = server.submitMessage(channelId, "Hello world!", new LinkedHashMap<>());
+ Message stored = server.fetchMessage(channelId, messageId);
+ assertNull(stored, () -> "DISABLED storage wrote a row for message " + messageId);
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MixedDestinationStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MixedDestinationStatisticsTest.java
new file mode 100644
index 0000000000..9909e22e50
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MixedDestinationStatisticsTest.java
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.counts;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * Destinations that end the same message differently are counted separately. Every message goes
+ * to all three destinations, so each destination receives the same three messages and the only
+ * thing that differs is the status it ends them in - which is the counter it credits. The channel
+ * aggregate then has to carry all three outcomes at once rather than only the last or the worst.
+ *
+ *
+ * - Source: a Channel Reader, which sends every message down the whole chain.
+ * - Destination 1 "Filtered": a filter rule that returns false, so its messages are filtered.
+ * - Destination 2 "Sent": a JavaScript Writer that returns a response.
+ * - Destination 3 "Error": a JavaScript Writer that throws, with its queue off and no
+ * retries, so the failure is final.
+ *
+ */
+@DisplayName("250-statistics/03-mixed-destination-outcomes")
+@TestInstance(PER_CLASS)
+class MixedDestinationStatisticsTest {
+
+ private static final String CHANNEL = "channels/statistics-mixed-destinations.xml";
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void eachDestinationCreditsItsOwnOutcome() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ long messageId = SharedServer.get().submitMessage(channelId, "mixed", new LinkedHashMap<>());
+ Harness.awaitProcessed(channelId, messageId);
+ }
+
+ assertStatistics(channelId, Map.of(
+ 0, counts(3, 0, 0, 0),
+ 1, counts(3, 3, 0, 0),
+ 2, counts(3, 0, 3, 0),
+ 3, counts(3, 0, 0, 3)), 0);
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java
index 51ded95d85..26d59a1ce5 100644
--- a/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java
@@ -6,6 +6,7 @@
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -14,10 +15,15 @@
import com.mirth.connect.donkey.model.channel.DeployedState;
import com.mirth.connect.donkey.model.message.Message;
import com.mirth.connect.donkey.model.message.RawMessage;
+import com.mirth.connect.donkey.model.message.Status;
+import com.mirth.connect.donkey.model.message.attachment.Attachment;
import com.mirth.connect.model.Channel;
+import com.mirth.connect.model.ChannelStatistics;
+import com.mirth.connect.model.DashboardStatus;
import com.mirth.connect.model.LoginStatus;
import com.mirth.connect.model.converters.ObjectXMLSerializer;
import com.mirth.connect.model.filters.MessageFilter;
+import com.mirth.connect.util.ConfigurationProperty;
import com.mirth.connect.util.MirthSSLUtil;
/**
@@ -88,7 +94,18 @@ private static synchronized void initSerializer(String serverVersion) throws Exc
* @return the deployed channel's id
*/
String deployChannel(String xml, String label) throws Exception {
- Channel channel = ObjectXMLSerializer.getInstance().deserialize(xml, Channel.class);
+ return deployChannel(ObjectXMLSerializer.getInstance().deserialize(xml, Channel.class), label);
+ }
+
+ /**
+ * Deploys a channel model, creating or overwriting whatever is on the server under its id.
+ * Deploying the same id twice redeploys it, which is how a test changes a channel in place.
+ *
+ * @param channel the channel to deploy
+ * @param label a human-readable name for the channel, used only in error messages
+ * @return the deployed channel's id
+ */
+ String deployChannel(Channel channel, String label) throws Exception {
String channelId = channel.getId();
if (channelId == null || channelId.isBlank()) {
throw new IllegalArgumentException("Channel fixture has no id: " + label);
@@ -98,7 +115,9 @@ String deployChannel(String xml, String label) throws Exception {
if (!client.createChannel(channel)) {
client.updateChannel(channel, true, null);
}
- deployedChannelIds.push(channelId);
+ if (!deployedChannelIds.contains(channelId)) {
+ deployedChannelIds.push(channelId);
+ }
// returnErrors=true so a deploy failure surfaces here instead of only as a status
// that never reaches STARTED. The String overload avoids DebuggerUtil parsing.
@@ -108,20 +127,75 @@ String deployChannel(String xml, String label) throws Exception {
}
private void awaitStarted(String channelId, String label) throws Exception {
+ try {
+ awaitState(channelId, DeployedState.STARTED);
+ } catch (AssertionError e) {
+ throw new AssertionError("Channel " + label + " (" + channelId + ") did not start: " + e.getMessage(), e);
+ }
+ }
+
+ private void awaitState(String channelId, DeployedState state) throws Exception {
long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos();
DeployedState lastState = null;
while (System.nanoTime() < deadline) {
- var status = client.getChannelStatus(channelId);
+ DashboardStatus status = client.getChannelStatus(channelId);
lastState = status == null ? null : status.getState();
- if (lastState == DeployedState.STARTED) {
+ if (lastState == state) {
return;
}
Thread.sleep(500);
}
- throw new AssertionError("Channel " + label + " (" + channelId + ") did not start within "
+ throw new AssertionError("Channel " + channelId + " did not reach " + state + " within "
+ HarnessConfig.TIMEOUT.toSeconds() + "s; last state was " + lastState);
}
+ /**
+ * The number of messages queued for one connector, as the dashboard reports it: the source
+ * queue for metadata id 0, a destination's queue otherwise. Returns null while the channel
+ * has no status for that connector.
+ */
+ Long queueSize(String channelId, int metaDataId) throws ClientException {
+ DashboardStatus status = client.getChannelStatus(channelId);
+ if (status == null) {
+ return null;
+ }
+ for (DashboardStatus connectorStatus : status.getChildStatuses()) {
+ if (Integer.valueOf(metaDataId).equals(connectorStatus.getMetaDataId())) {
+ return connectorStatus.getQueued();
+ }
+ }
+ return Integer.valueOf(metaDataId).equals(status.getMetaDataId()) ? status.getQueued() : null;
+ }
+
+ /** Stops a channel, leaving it deployed, and waits for it to report {@link DeployedState#STOPPED}. */
+ void stopChannel(String channelId) throws Exception {
+ client.stopChannel(channelId, true);
+ awaitState(channelId, DeployedState.STOPPED);
+ }
+
+ /**
+ * Halts a channel, which interrupts whatever it is processing instead of waiting for it, and
+ * waits for it to report {@link DeployedState#STOPPED}.
+ */
+ void haltChannel(String channelId) throws Exception {
+ client.haltChannel(channelId, true);
+ awaitState(channelId, DeployedState.STOPPED);
+ }
+
+ /**
+ * Starts a channel and waits for it to report {@link DeployedState#STARTED}, doing nothing if
+ * it is already started. Tolerating that lets a test restore a channel it stopped from a
+ * {@code finally} without having to know whether the stop got that far.
+ */
+ void startChannel(String channelId) throws Exception {
+ DashboardStatus status = client.getChannelStatus(channelId);
+ if (status != null && status.getState() == DeployedState.STARTED) {
+ return;
+ }
+ client.startChannel(channelId, true);
+ awaitState(channelId, DeployedState.STARTED);
+ }
+
/** Submits a source payload and returns the new message id. */
long submitMessage(String channelId, String rawData, Map sourceMap) throws ClientException {
RawMessage rawMessage = new RawMessage(rawData, null, sourceMap);
@@ -132,7 +206,24 @@ long submitMessage(String channelId, String rawData, Map sourceM
return messageId;
}
- /** Reads one message back, with content, so assertions can inspect every connector. */
+ /**
+ * Sets one configuration map entry, leaving the rest alone. This is the only server-side
+ * state a client can write that channel scripts can read back, which makes it the harness's
+ * way to signal a running script.
+ *
+ * The server does the read-modify-write under its own lock, so test classes running in
+ * parallel can set their own gate properties without dropping each other's.
+ */
+ void setConfigurationProperty(String key, String value) throws ClientException {
+ client.setConfigurationProperty(key, new ConfigurationProperty(value, null));
+ }
+
+ /**
+ * Reads one message back, with content, so assertions can inspect every connector. Asking
+ * for the content switches the server's DAO out of decrypting mode, so the content is
+ * whatever is stored - ciphertext for a channel with {@code encryptData} set. See
+ * {@link #fetchDecryptedMessage}.
+ */
Message fetchMessage(String channelId, long messageId) throws ClientException {
MessageFilter filter = new MessageFilter();
filter.setMinMessageId(messageId);
@@ -145,6 +236,88 @@ Message fetchMessage(String channelId, long messageId) throws ClientException {
return messages.get(0);
}
+ /**
+ * Reads one message back through the decrypting path. {@link #fetchMessage} asks the server
+ * for the content as stored, so an encrypting channel's content arrives as ciphertext; this
+ * call leaves the DAO decrypting, which is what the administrator sees in the message browser.
+ */
+ Message fetchDecryptedMessage(String channelId, long messageId, List metaDataIds)
+ throws ClientException {
+ return client.getMessageContent(channelId, messageId, metaDataIds);
+ }
+
+ /**
+ * Every attachment stored against one message, with its content. Attachments live in their
+ * own table rather than on the message, so they never appear in {@link #fetchMessage}; the
+ * harness makes this second read only when a fixture names an {@code attachmentNN} file.
+ */
+ List fetchAttachments(String channelId, long messageId) throws ClientException {
+ List attachments = client.getAttachmentsByMessageId(channelId, messageId);
+ return attachments == null ? List.of() : attachments;
+ }
+
+ /**
+ * Deletes one message, or one of its connector messages when {@code metaDataId} is not null.
+ * This is the message browser's delete: the server turns it into a one-message filter, so
+ * asking for metadata id 0 deletes the whole message rather than only the source.
+ */
+ void removeMessage(String channelId, long messageId, Integer metaDataId) throws ClientException {
+ client.removeMessage(channelId, messageId, metaDataId, null);
+ }
+
+ /**
+ * Deletes every message in a channel, stopping and restarting it if it is running, and
+ * optionally resetting its statistics at the same time.
+ */
+ void removeAllMessages(String channelId, boolean clearStatistics) throws ClientException {
+ client.removeAllMessages(channelId, true, clearStatistics);
+ }
+
+ /** How many messages the channel still holds. */
+ long messageCount(String channelId) throws ClientException {
+ Long count = client.getMessageCount(channelId, new MessageFilter());
+ return count == null ? 0L : count;
+ }
+
+ /**
+ * The channel's aggregate lifetime counters, as the dashboard's statistics view reports them.
+ *
+ * For a deployed channel these come from the running engine's own tallies. For a channel
+ * that has been undeployed without being removed they come from the database, which is the
+ * only way a client sees what was actually stored.
+ */
+ ChannelStatistics statistics(String channelId) throws ClientException {
+ return client.getStatistics(channelId);
+ }
+
+ /** Undeploys a channel without removing it, so its stored rows stay where they are. */
+ void undeployChannel(String channelId) throws ClientException {
+ client.undeployChannel(channelId, true);
+ }
+
+ /**
+ * Every statistics counter the dashboard shows for one channel, keyed by connector: 0 for the
+ * source, its metadata id for each destination, and null for the channel's own aggregate row.
+ * Only the statuses in {@code com.mirth.connect.donkey.server.channel.Statistics}'s
+ * {@code TRACKED_STATUSES} are counted, so each map holds RECEIVED, FILTERED, SENT and ERROR
+ * and nothing else.
+ */
+ Map> connectorStatistics(String channelId) throws ClientException {
+ DashboardStatus status = client.getChannelStatus(channelId);
+ if (status == null) {
+ throw new AssertionError("Channel " + channelId + " has no dashboard status");
+ }
+
+ Map> statistics = new LinkedHashMap<>();
+ // The channel's own status carries the aggregate row, which the table stores under a null
+ // metadata id; its children carry the per-connector rows.
+ statistics.put(null, status.getStatistics());
+ for (DashboardStatus connectorStatus : status.getChildStatuses()) {
+ statistics.put(connectorStatus.getMetaDataId(), connectorStatus.getStatistics());
+ }
+ return statistics;
+ }
+
/** Undeploys and removes a channel, tolerating failures so teardown always continues. */
void removeChannel(String channelId) {
deployedChannelIds.remove(channelId);
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/PersistedStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/PersistedStatisticsTest.java
new file mode 100644
index 0000000000..53b460694d
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/PersistedStatisticsTest.java
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.counts;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.model.ChannelStatistics;
+
+/**
+ * The counters a running channel reports are held in memory and written to the database behind
+ * it, and every other case here reads the memory. This one reads the database.
+ *
+ * That matters because the statement that writes them is one of the few the engine keeps a
+ * per-dialect version of - derby cannot use the {@code GREATEST} the others do and falls back to
+ * a {@code CASE} rewrite - and a write nothing reads back is a write nothing checks. A client
+ * only ever sees the stored rows for a channel that is not deployed, because a deployed one is
+ * always answered from the engine's own tallies, so the channel is undeployed without being
+ * removed and asked again.
+ *
+ *
The message mixture is the one from
+ * {@link MixedDestinationStatisticsTest}, so all four stored counters are non-zero and a write
+ * that lost or crossed a column has somewhere to show up.
+ *
+ *
+ * - Source: a Channel Reader.
+ * - Destination 1 "Filtered": a filter rule that returns false.
+ * - Destination 2 "Sent": a JavaScript Writer that returns a response.
+ * - Destination 3 "Error": a JavaScript Writer that throws, with no queue and no retries.
+ *
+ */
+@DisplayName("250-statistics/05-written-to-the-database")
+@TestInstance(PER_CLASS)
+class PersistedStatisticsTest {
+
+ private static final String CHANNEL = "channels/statistics-persisted.xml";
+
+ private static final int MESSAGE_COUNT = 3;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void theStoredCountersAreTheOnesTheRunningChannelReported() throws Exception {
+ for (int i = 0; i < MESSAGE_COUNT; i++) {
+ long messageId = SharedServer.get().submitMessage(channelId, "persisted", new LinkedHashMap<>());
+ Harness.awaitProcessed(channelId, messageId);
+ }
+
+ assertStatistics(channelId, Map.of(
+ 0, counts(MESSAGE_COUNT, 0, 0, 0),
+ 1, counts(MESSAGE_COUNT, MESSAGE_COUNT, 0, 0),
+ 2, counts(MESSAGE_COUNT, 0, MESSAGE_COUNT, 0),
+ 3, counts(MESSAGE_COUNT, 0, 0, MESSAGE_COUNT)), 0);
+
+ Harness.undeployChannel(channelId);
+
+ // The engine writes the counters out on a timer, so the stored rows can be a moment
+ // behind the channel that has just stopped reporting them.
+ awaitStoredStatistics(MESSAGE_COUNT, MESSAGE_COUNT, MESSAGE_COUNT, MESSAGE_COUNT);
+ }
+
+ /** Polls the stored counters until they hold what the running channel reported. */
+ private void awaitStoredStatistics(long received, long filtered, long sent, long error) throws Exception {
+ List expected = List.of(received, filtered, sent, error);
+ long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos();
+ List actual = null;
+ do {
+ ChannelStatistics stored = SharedServer.get().statistics(channelId);
+ actual = List.of(stored.getReceived(), stored.getFiltered(), stored.getSent(), stored.getError());
+ if (expected.equals(actual)) {
+ return;
+ }
+ Thread.sleep(250);
+ } while (System.nanoTime() < deadline);
+
+ assertEquals(expected, actual, "the counters the engine stored are not the ones it reported");
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/QueuedMessageDeletionTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/QueuedMessageDeletionTest.java
new file mode 100644
index 0000000000..a8bba03c06
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/QueuedMessageDeletionTest.java
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * Deleting messages a destination queue is still holding. A queue is a database table and an
+ * in-memory buffer at once, so a delete that only reaches the table leaves the queue thread
+ * retrying rows that are no longer there; the server is supposed to invalidate the queue as part
+ * of the delete, which is what makes the dashboard's depth fall to zero.
+ *
+ * The destination fails every send while the gate is shut, so the messages are on the queue and
+ * staying there when the delete lands, rather than racing the queue thread to be drained first.
+ */
+@DisplayName("240-message-deletion/02-queued-destination")
+@TestInstance(PER_CLASS)
+class QueuedMessageDeletionTest {
+
+ private static final String CHANNEL = "channels/message-deletion-queued.xml";
+
+ /** The configuration property the destination fails on until it holds this run's token. */
+ private static final String GATE = "oie.smoketest.releaseDeletionQueue";
+
+ private static final int DESTINATION = 1;
+
+ private static final int MESSAGE_COUNT = 4;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void deletingQueuedMessagesEmptiesTheQueue() throws Exception {
+ OieServer server = SharedServer.get();
+ // The payload carries this run's token, so a gate left open by an earlier run cannot
+ // release this one.
+ String token = UUID.randomUUID().toString();
+ List messageIds = new ArrayList<>();
+
+ try {
+ for (int i = 1; i <= MESSAGE_COUNT; i++) {
+ messageIds.add(server.submitMessage(channelId, token, new LinkedHashMap<>()));
+ }
+ Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT);
+
+ for (long messageId : messageIds) {
+ server.removeMessage(channelId, messageId, null);
+ }
+
+ assertEquals(0, server.messageCount(channelId), "the queued messages were not deleted");
+ Harness.awaitQueueSizeAtMost(channelId, DESTINATION, 0);
+ } finally {
+ // Also on failure, so a channel left behind by this test is not a wedged one.
+ Harness.setConfigurationProperty(GATE, token);
+ }
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceFilteredStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceFilteredStatisticsTest.java
new file mode 100644
index 0000000000..da2c19254e
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceFilteredStatisticsTest.java
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.counts;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * What a channel counts when its source filter rejects everything. A message the source filters
+ * never reaches a destination, so the destination's counters have to stay at zero while the
+ * channel still counts the message as both received and filtered - RECEIVED is what came in, not
+ * what survived, and the two are separate counters rather than one moving between them.
+ *
+ *
+ * - Source: a Channel Reader with a filter rule that returns false for every message.
+ * - Destination 1: a JavaScript Writer that would send, and never gets the chance.
+ *
+ */
+@DisplayName("250-statistics/02-filtered-at-the-source")
+@TestInstance(PER_CLASS)
+class SourceFilteredStatisticsTest {
+
+ private static final String CHANNEL = "channels/statistics-source-filtered.xml";
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void aMessageFilteredAtTheSourceIsCountedThereAndNowhereElse() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ long messageId = SharedServer.get().submitMessage(channelId, "rejected", new LinkedHashMap<>());
+ Harness.awaitProcessed(channelId, messageId);
+ }
+
+ assertStatistics(channelId, Map.of(
+ 0, counts(3, 3, 0, 0),
+ 1, counts(0, 0, 0, 0)), 0);
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceQueueTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceQueueTest.java
new file mode 100644
index 0000000000..d91bb610d1
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceQueueTest.java
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import com.mirth.connect.donkey.model.message.ConnectorMessage;
+import com.mirth.connect.donkey.model.message.Status;
+
+/**
+ * The source queue: that it keeps its order once it is deeper than the buffer it holds in
+ * memory, that it survives the channel it belongs to being halted, and that concurrent
+ * submitters cannot lose a message between committing it and queueing it.
+ *
+ * The channel sets {@code queueBufferSize} to 3 and every test here queues more than that,
+ * so refilling the buffer from the database is the path most messages take rather than an edge
+ * case. Its destination parks until the gate property holds the run's token, which is what lets
+ * a queue build up before anything drains; the token is fresh per run, so a gate left open by an
+ * earlier run cannot release a later one.
+ *
+ *
The destination stamps each message with the position it was handed over in, taken from a
+ * counter in the global channel map. Assertions are on the relative positions - a
+ * consecutive ascending run - because the counter carries across tests and resets when the
+ * channel restarts, and because relative positions are exactly what the ordering claim is.
+ */
+@DisplayName("220-queueing/01-source-queue")
+@TestInstance(PER_CLASS)
+class SourceQueueTest {
+
+ private static final String CHANNEL = "channels/source-queue-order.xml";
+
+ /** The configuration property the destination parks on. */
+ private static final String GATE = "oie.smoketest.releaseSourceQueue";
+
+ /** What the destination prefixes its handover position with. */
+ private static final String POSITION = "order=";
+
+ private static final int SOURCE = 0;
+ private static final int DESTINATION = 1;
+
+ /** {@code queueBufferSize} in the channel: how much of the source queue is held in memory. */
+ private static final int BUFFER_CAPACITY = 3;
+
+ private static final int MESSAGE_COUNT = 10;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ /**
+ * A queue deeper than its buffer still hands every message to the destination exactly once
+ * and in the order they were received, which means the refill from the database picks up
+ * where the buffer left off rather than starting over or skipping.
+ */
+ @Test
+ void drainsInReceiptOrderPastItsBufferCapacity() throws Exception {
+ String token = UUID.randomUUID().toString();
+ List messageIds = new ArrayList<>();
+
+ try {
+ for (int i = 1; i <= MESSAGE_COUNT; i++) {
+ messageIds.add(SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>()));
+ }
+
+ // The first message is parked in the destination and the rest are behind it. Waiting
+ // for the queue to pass its buffer is what makes the refill certain rather than hoped
+ // for; if it never happens the wait fails the test.
+ Harness.awaitQueueSizeAtLeast(channelId, SOURCE, BUFFER_CAPACITY + 1);
+ } finally {
+ // Also on failure, so a parked destination never outlives the test.
+ Harness.setConfigurationProperty(GATE, token);
+ }
+
+ assertConsecutive(handoverPositions(messageIds),
+ "the source queue should drain in the order the messages were received");
+ }
+
+ /**
+ * Halting a channel with a full source queue leaves the queue alone, and starting it again
+ * drains it in order. A halt rather than a stop because a stop waits for the message the
+ * destination is parked on, which is the one thing this test needs to still be parked.
+ *
+ * That the queue thread is not running in between is not asserted - it cannot be, over an
+ * API that only reports state. What is asserted is the consequence that matters: the queue
+ * was still there afterwards, and the restarted channel reloaded and drained it.
+ */
+ @Test
+ void survivesAHaltedChannelAndDrainsWhenItStartsAgain() throws Exception {
+ String token = UUID.randomUUID().toString();
+ List messageIds = new ArrayList<>();
+
+ try {
+ for (int i = 1; i <= MESSAGE_COUNT; i++) {
+ messageIds.add(SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>()));
+ }
+ // One message is in the destination, so the rest are the queue.
+ int queued = MESSAGE_COUNT - 1;
+ Harness.awaitQueueSizeAtLeast(channelId, SOURCE, queued);
+
+ Harness.haltChannel(channelId);
+
+ assertEquals(Long.valueOf(queued), SharedServer.get().queueSize(channelId, SOURCE),
+ "halting the channel should leave its source queue intact, not drain or drop it");
+ } finally {
+ // Also on failure, so neither a parked destination nor a halted channel outlives the
+ // test. Starting an already-started channel is a no-op, so this is safe either way.
+ Harness.setConfigurationProperty(GATE, token);
+ Harness.startChannel(channelId);
+ }
+
+ assertConsecutive(handoverPositions(messageIds),
+ "the restarted channel should drain the queue it reloaded, in order");
+ }
+
+ /**
+ * Several connections dispatching at once still get every message processed exactly once.
+ * The engine commits a source message and adds it to the queue under the queue's own lock;
+ * without that, a message committed but not yet queued is picked up by another thread's
+ * buffer refill and the queue's count and contents disagree, stranding one of them.
+ * {@code ConnectorMessageQueueTest} pins that mechanism down directly - this is the same
+ * claim where a client can see it, so it catches the lock going missing rather than
+ * describing what happens when it does.
+ *
+ * A connection serialises its own requests, so each submitter needs its own; sharing one
+ * would serialise the very dispatch this is about.
+ */
+ @Test
+ void concurrentSubmittersEachGetTheirMessageProcessedExactlyOnce() throws Exception {
+ String token = UUID.randomUUID().toString();
+ int submitters = 3;
+ int perSubmitter = 8;
+
+ List messageIds = new ArrayList<>();
+ List connections = new ArrayList<>();
+ ExecutorService submitterPool = Executors.newFixedThreadPool(submitters);
+
+ try {
+ List>> submitted = new ArrayList<>();
+ for (int submitter = 0; submitter < submitters; submitter++) {
+ OieServer connection = OieServer.connect();
+ connections.add(connection);
+
+ String prefix = token + "#" + submitter + "-";
+ submitted.add(submitterPool.submit(() -> {
+ List mine = new ArrayList<>();
+ for (int i = 1; i <= perSubmitter; i++) {
+ mine.add(connection.submitMessage(channelId, prefix + i, new LinkedHashMap<>()));
+ }
+ return mine;
+ }));
+ }
+ for (Future> batch : submitted) {
+ messageIds.addAll(batch.get());
+ }
+
+ Harness.awaitQueueSizeAtLeast(channelId, SOURCE, BUFFER_CAPACITY + 1);
+ } finally {
+ Harness.setConfigurationProperty(GATE, token);
+ submitterPool.shutdown();
+ connections.forEach(OieServer::close);
+ }
+
+ // Which submitter won a given position is a race and not asserted; that every message got
+ // a position, and no two got the same one, is the claim.
+ List positions = handoverPositions(messageIds);
+ Collections.sort(positions);
+ assertEquals(submitters * perSubmitter, positions.size(), "a message went missing");
+ assertConsecutive(positions, "every concurrently submitted message should be handed over"
+ + " exactly once, so their positions are distinct and leave no gap");
+ }
+
+ /**
+ * Waits for every message to be sent and returns the position its destination stamped it
+ * with, in the order the server committed them - which is message id order, since the id is
+ * assigned by the insert.
+ */
+ private List handoverPositions(List messageIds) throws Exception {
+ List inCommitOrder = new ArrayList<>(messageIds);
+ Collections.sort(inCommitOrder);
+
+ List positions = new ArrayList<>();
+ for (long messageId : inCommitOrder) {
+ ConnectorMessage sent = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT);
+ String stamp = MessageAssertions.responsePayload(sent.getResponse().getContent());
+ assertTrue(stamp != null && stamp.startsWith(POSITION),
+ "message " + messageId + " has an unexpected destination response: " + stamp);
+ positions.add(Integer.valueOf(stamp.substring(POSITION.length())));
+ }
+ return positions;
+ }
+
+ /**
+ * Asserts the positions are a consecutive ascending run. Where they start is the channel
+ * counter's business; that they rise by one each time is the queue's.
+ */
+ private static void assertConsecutive(List positions, String message) {
+ List expected = new ArrayList<>();
+ for (int i = 0; i < positions.size(); i++) {
+ expected.add(positions.get(0) + i);
+ }
+ assertEquals(expected, positions, message);
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/StalledDestinationStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/StalledDestinationStatisticsTest.java
new file mode 100644
index 0000000000..01903c71a6
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/StalledDestinationStatisticsTest.java
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.counts;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * What a channel reports while a destination is stalled. A queued message has been received by
+ * the destination but not sent, and the queue it is sitting on is a depth the server measures
+ * rather than a counter it keeps: the destination's RECEIVED rises as soon as the message reaches
+ * it, SENT stays where it was until the destination actually accepts the message, and the queue
+ * depth is what carries the difference in the meantime.
+ *
+ *
+ * - Source: a Channel Reader.
+ * - Destination 1: a JavaScript Writer with its queue on, which returns QUEUED - a send failure
+ * the queue retries, not an error - until the configuration map holds this run's token.
+ *
+ */
+@DisplayName("250-statistics/04-stalled-destination")
+@TestInstance(PER_CLASS)
+class StalledDestinationStatisticsTest {
+
+ private static final String CHANNEL = "channels/statistics-queued-destination.xml";
+
+ /** The configuration property the destination stalls on until it holds this run's token. */
+ private static final String GATE = "oie.smoketest.releaseStatisticsQueue";
+
+ private static final int DESTINATION = 1;
+
+ private static final int MESSAGE_COUNT = 3;
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void aStalledDestinationReportsItsQueueDepthUntilItSends() throws Exception {
+ // The payload is this run's token, so a gate left open by an earlier run cannot release
+ // this one, and every message carries the same token so one write releases them all.
+ String token = UUID.randomUUID().toString();
+
+ try {
+ for (int i = 0; i < MESSAGE_COUNT; i++) {
+ SharedServer.get().submitMessage(channelId, token, new LinkedHashMap<>());
+ }
+ // The depth is a precondition the test enforces rather than one it hopes for: nothing
+ // below is read until the server itself says all three messages are on the queue.
+ Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT);
+
+ assertStatistics(channelId, Map.of(
+ 0, counts(MESSAGE_COUNT, 0, 0, 0),
+ DESTINATION, counts(MESSAGE_COUNT, 0, 0, 0)), MESSAGE_COUNT);
+ } finally {
+ // Also on failure, so a channel left behind by this test is not a wedged one.
+ Harness.setConfigurationProperty(GATE, token);
+ }
+
+ Harness.awaitQueueSizeAtMost(channelId, DESTINATION, 0);
+
+ assertStatistics(channelId, Map.of(
+ 0, counts(MESSAGE_COUNT, 0, 0, 0),
+ DESTINATION, counts(MESSAGE_COUNT, 0, MESSAGE_COUNT, 0)), 0);
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAccountingTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAccountingTest.java
new file mode 100644
index 0000000000..455aac7bfa
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAccountingTest.java
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics;
+import static org.openintegrationengine.smoketest.StatisticsAssertions.counts;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * What a channel counts when nothing goes wrong. The channel has two destinations that both
+ * accept every message, so the source and each destination keep their own counters and the
+ * channel's aggregate SENT is the two destinations added together rather than either one of them.
+ *
+ * Statistics are a channel-wide running total rather than a property of a message, so there is
+ * no fixture shape for them; every case under {@code 250-statistics} reads them back over the
+ * client API instead.
+ *
+ *
+ * - Source: a Channel Reader, so the harness submits the messages itself.
+ * - Destinations 1 and 2: JavaScript Writers that return a response, which is a send.
+ *
+ */
+@DisplayName("250-statistics/01-normal-flow")
+@TestInstance(PER_CLASS)
+class StatisticsAccountingTest {
+
+ private static final String CHANNEL = "channels/statistics-normal-flow.xml";
+
+ private String channelId;
+
+ @BeforeAll
+ void deploy() throws Exception {
+ channelId = Harness.deploy(CHANNEL);
+ }
+
+ @AfterAll
+ void undeploy() {
+ Harness.undeploy(channelId);
+ }
+
+ @Test
+ void everyConnectorCountsTheMessagesItHandled() throws Exception {
+ submit();
+
+ // Asserted after one message and again after three, so a counter that was assigned the
+ // message count rather than incremented per message would still have to be caught.
+ assertStatistics(channelId, Map.of(
+ 0, counts(1, 0, 0, 0),
+ 1, counts(1, 0, 1, 0),
+ 2, counts(1, 0, 1, 0)), 0);
+
+ submit();
+ submit();
+
+ assertStatistics(channelId, Map.of(
+ 0, counts(3, 0, 0, 0),
+ 1, counts(3, 0, 3, 0),
+ 2, counts(3, 0, 3, 0)), 0);
+ }
+
+ /** Submits one message and waits for the server to finish with it, counters included. */
+ private void submit() throws Exception {
+ long messageId = SharedServer.get().submitMessage(channelId, "counted", new LinkedHashMap<>());
+ Harness.awaitProcessed(channelId, messageId);
+ }
+}
diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAssertions.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAssertions.java
new file mode 100644
index 0000000000..791a502556
--- /dev/null
+++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAssertions.java
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: 2026 Mitch Gaffigan
+
+package org.openintegrationengine.smoketest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.mirth.connect.donkey.model.message.Status;
+import com.mirth.connect.model.ChannelStatistics;
+
+/**
+ * Statistics assertions shared by the cases under {@code 250-statistics}.
+ *
+ * A channel's counters reach a client two ways, computed separately by the server. The
+ * dashboard reports a row per connector plus the channel's own aggregate row, which the engine
+ * maintains as it goes; the statistics view reports one {@link ChannelStatistics} the server
+ * re-adds up from the connector rows on each call. A test here states the per-connector counters
+ * it expects and {@link #assertStatistics} derives both aggregates from them, so each case pins
+ * down the connector counters it is about and every case cross-checks the two aggregates against
+ * the same rule:
+ *
+ *
+ * - RECEIVED counts the source connector only.
+ * - SENT counts the destination connectors only.
+ * - FILTERED and ERROR count every connector.
+ *
+ *
+ * Only the four statuses in
+ * {@code com.mirth.connect.donkey.server.channel.Statistics.TRACKED_STATUSES} are counted at all;
+ * a queue is a depth the server measures when asked rather than a counter, which is why it is a
+ * separate argument.
+ */
+final class StatisticsAssertions {
+
+ /**
+ * How long to keep re-reading before giving up on a counter. A connector's status and its
+ * counters are committed together but read back separately, so a caller that waited for a
+ * status can still be a moment early for the counter it implies.
+ */
+ private static final Duration GRACE = Duration.ofSeconds(5);
+
+ private static final Duration POLL_INTERVAL = Duration.ofMillis(250);
+
+ private StatisticsAssertions() {
+ }
+
+ /** One connector's counters, in the order the engine tracks them. */
+ static Map counts(long received, long filtered, long sent, long error) {
+ Map counts = new LinkedHashMap<>();
+ counts.put(Status.RECEIVED, received);
+ counts.put(Status.FILTERED, filtered);
+ counts.put(Status.SENT, sent);
+ counts.put(Status.ERROR, error);
+ return counts;
+ }
+
+ /**
+ * Asserts every counter the server reports for one channel: the per-connector rows against
+ * {@code expected}, keyed by metadata id, and both aggregates against the roll-up of
+ * {@code expected} described above. Retries for {@link #GRACE} before failing.
+ *
+ * @param expectedQueued how many messages should be sitting on the channel's queues
+ */
+ static void assertStatistics(String channelId, Map> expected, long expectedQueued)
+ throws Exception {
+ long deadline = System.nanoTime() + GRACE.toNanos();
+ while (true) {
+ try {
+ assertStatisticsNow(channelId, expected, expectedQueued);
+ return;
+ } catch (AssertionError e) {
+ if (System.nanoTime() >= deadline) {
+ throw e;
+ }
+ }
+ Thread.sleep(POLL_INTERVAL.toMillis());
+ }
+ }
+
+ private static void assertStatisticsNow(String channelId, Map> expected,
+ long expectedQueued) throws Exception {
+ Map> actual = SharedServer.get().connectorStatistics(channelId);
+
+ Map> actualConnectors = new LinkedHashMap<>(actual);
+ Map actualAggregate = actualConnectors.remove(null);
+ assertEquals(expected, actualConnectors, "the per-connector statistics are wrong");
+
+ Map expectedAggregate = rollUp(expected);
+ assertEquals(expectedAggregate, actualAggregate, "the channel's aggregate statistics row is wrong");
+
+ // The statistics view adds the connector rows up again on its own, so it has to agree.
+ ChannelStatistics statistics = SharedServer.get().statistics(channelId);
+ assertEquals(
+ List.of(expectedAggregate.get(Status.RECEIVED), expectedAggregate.get(Status.FILTERED),
+ expectedAggregate.get(Status.SENT), expectedAggregate.get(Status.ERROR), expectedQueued),
+ List.of(statistics.getReceived(), statistics.getFiltered(), statistics.getSent(),
+ statistics.getError(), statistics.getQueued()),
+ () -> "the statistics view disagrees with the dashboard; it reported " + statistics);
+ }
+
+ /** Adds the per-connector counters up the way the channel's own aggregate is defined. */
+ private static Map rollUp(Map> connectors) {
+ long received = 0;
+ long filtered = 0;
+ long sent = 0;
+ long error = 0;
+
+ for (Map.Entry> connector : connectors.entrySet()) {
+ boolean source = connector.getKey() == 0;
+ Map counts = connector.getValue();
+ received += source ? counts.get(Status.RECEIVED) : 0;
+ sent += source ? 0 : counts.get(Status.SENT);
+ filtered += counts.get(Status.FILTERED);
+ error += counts.get(Status.ERROR);
+ }
+
+ return counts(received, filtered, sent, error);
+ }
+}
diff --git a/smoketest/src/test/resources/channels/blocking-response-transformer.xml b/smoketest/src/test/resources/channels/blocking-response-transformer.xml
new file mode 100644
index 0000000000..8c800757d0
--- /dev/null
+++ b/smoketest/src/test/resources/channels/blocking-response-transformer.xml
@@ -0,0 +1,199 @@
+
+ 6a9c1b80-2d34-4f57-8a61-93b7c05de266
+ 2
+ blocking-response-transformer
+
+ 1
+
+ 0
+ sourceConnector
+
+