From bcbfc2c56e419ca1cce10f61d23c9d485a61f3d0 Mon Sep 17 00:00:00 2001 From: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:35:56 -0700 Subject: [PATCH 1/2] Write the Alert Dispatched event when an alert value is null AlertWorker.ActionTask built the event by calling toString() on every alert context value, so a null value threw after the alert's actions had gone out. Nothing reads the task's Future, so the event was dropped and nothing was logged. A blank server name (since #259) and a null connector type (source transformer errors, alerts.sendAlert) both hit it. Null values now become empty attributes, as AuthorizationController does for audit events, and a failure in the event step is logged like the protocol and email steps already are. Closes #572 Signed-off-by: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com> Co-Authored-By: Claude --- .../connect/server/alert/AlertWorker.java | 14 +- .../connect/server/alert/AlertWorkerTest.java | 126 ++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java diff --git a/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java b/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java index 4de7603b73..0c89828c34 100644 --- a/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java +++ b/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -207,12 +208,15 @@ public Void call() throws Exception { } // Dispatch a server event to notify that an alert was dispatched - ServerEvent serverEvent = new ServerEvent(serverId, "Alert Dispatched"); - for (Entry entry : context.entrySet()) { - String value = entry.getValue().toString(); - serverEvent.addAttribute(entry.getKey(), value); + try { + ServerEvent serverEvent = new ServerEvent(serverId, "Alert Dispatched"); + for (Entry entry : context.entrySet()) { + serverEvent.addAttribute(entry.getKey(), Objects.toString(entry.getValue(), "")); + } + eventController.dispatchEvent(serverEvent); + } catch (Exception e) { + logger.error("Error dispatching the Alert Dispatched event.", e); } - eventController.dispatchEvent(serverEvent); } return null; diff --git a/server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java b/server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java new file mode 100644 index 0000000000..34b22a0183 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Open Integration Engine + +package com.mirth.connect.server.alert; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.apache.logging.log4j.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import com.mirth.connect.donkey.model.event.Event; +import com.mirth.connect.donkey.testing.StaticOverride; +import com.mirth.connect.model.ServerEvent; +import com.mirth.connect.model.alert.AlertActionGroup; +import com.mirth.connect.model.alert.AlertModel; +import com.mirth.connect.server.controllers.AlertController; +import com.mirth.connect.server.controllers.ConfigurationController; +import com.mirth.connect.server.controllers.ControllerFactory; +import com.mirth.connect.server.controllers.EventController; +import com.mirth.connect.server.controllers.ExtensionController; + +public class AlertWorkerTest { + + private static final String SERVER_ID = "0000-0000-0000"; + + private StaticOverride factoryOverride; + private ConfigurationController configurationController; + private EventController eventController; + private DefaultAlertWorker worker; + private AlertActionGroup actionGroup; + private Alert alert; + + @Before + public void setup() { + ControllerFactory controllerFactory = mock(ControllerFactory.class); + configurationController = mock(ConfigurationController.class); + eventController = mock(EventController.class); + when(controllerFactory.createConfigurationController()).thenReturn(configurationController); + when(controllerFactory.createEventController()).thenReturn(eventController); + when(controllerFactory.createExtensionController()).thenReturn(mock(ExtensionController.class)); + when(controllerFactory.createAlertController()).thenReturn(mock(AlertController.class)); + when(configurationController.getServerId()).thenReturn(SERVER_ID); + factoryOverride = StaticOverride.install(ControllerFactory.class, "factory", controllerFactory); + + worker = new DefaultAlertWorker(); + actionGroup = new AlertActionGroup(); + AlertModel model = new AlertModel(null, actionGroup); + model.setName("Test Alert"); + worker.enableAlert(model); + alert = worker.getEnabledAlert(model.getId()); + } + + @After + public void teardown() { + if (worker != null) { + worker.shutdown(); + } + if (factoryOverride != null) { + factoryOverride.close(); + } + } + + @Test + public void writesEventWhenServerNameIsNotSet() throws Exception { + // getServerName() is not stubbed, so it returns null, as it does on a server with no name set + Map context = alert.createContext(); + // Filter, transformer and alerts.sendAlert errors carry no connector type + context.put("connectorType", null); + context.put("channelName", "Throws Error"); + + runTask(context); + + ServerEvent event = captureEvent(); + assertEquals("Alert Dispatched", event.getName()); + assertEquals(SERVER_ID, event.getServerId()); + Map attributes = event.getAttributes(); + assertEquals("", attributes.get("serverName")); + assertEquals("", attributes.get("connectorType")); + assertEquals("Test Alert", attributes.get("alertName")); + assertEquals("Throws Error", attributes.get("channelName")); + } + + @Test + public void writesServerNameWhenSet() throws Exception { + when(configurationController.getServerName()).thenReturn("Server A"); + + runTask(alert.createContext()); + + assertEquals("Server A", captureEvent().getAttributes().get("serverName")); + } + + @Test + public void logsWhenTheEventCannotBeDispatched() throws Exception { + when(configurationController.getServerName()).thenReturn("Server A"); + RuntimeException failure = new RuntimeException("event queue unavailable"); + doThrow(failure).when(eventController).dispatchEvent(any(Event.class)); + Logger logger = mock(Logger.class); + worker.logger = logger; + + runTask(alert.createContext()); + + verify(logger).error(eq("Error dispatching the Alert Dispatched event."), same(failure)); + } + + private void runTask(Map context) throws Exception { + worker.new ActionTask(alert.getModel().getId(), actionGroup, context).call(); + } + + private ServerEvent captureEvent() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Event.class); + verify(eventController).dispatchEvent(captor.capture()); + return (ServerEvent) captor.getValue(); + } +} From 7f9ca4ffca98239ebc59ee365f0a07296b44ae56 Mon Sep 17 00:00:00 2001 From: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:01:02 -0700 Subject: [PATCH 2/2] Name the alert in the Alert Dispatched error log If writing the Alert Dispatched event fails, the error now says which alert it was, as "name" (id), the way the engine names channels in its log lines. The exception is still attached. Signed-off-by: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com> Co-Authored-By: Claude --- .../main/java/com/mirth/connect/server/alert/AlertWorker.java | 2 +- .../java/com/mirth/connect/server/alert/AlertWorkerTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java b/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java index 0c89828c34..de391f230e 100644 --- a/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java +++ b/server/src/main/java/com/mirth/connect/server/alert/AlertWorker.java @@ -215,7 +215,7 @@ public Void call() throws Exception { } eventController.dispatchEvent(serverEvent); } catch (Exception e) { - logger.error("Error dispatching the Alert Dispatched event.", e); + logger.error("Error dispatching the Alert Dispatched event for alert \"" + alert.getModel().getName() + "\" (" + alertId + ").", e); } } diff --git a/server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java b/server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java index 34b22a0183..61c8610398 100644 --- a/server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java +++ b/server/src/test/java/com/mirth/connect/server/alert/AlertWorkerTest.java @@ -111,7 +111,8 @@ public void logsWhenTheEventCannotBeDispatched() throws Exception { runTask(alert.createContext()); - verify(logger).error(eq("Error dispatching the Alert Dispatched event."), same(failure)); + verify(logger).error(eq("Error dispatching the Alert Dispatched event for alert \"Test Alert\" (" + alert.getModel().getId() + ")."), + same(failure)); } private void runTask(Map context) throws Exception {