From b638741c61ca291e87b954ff6e0e9dd062c32b72 Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 15 Oct 2026 07:25:44 -0500 Subject: [PATCH] fix(alerts): asSystem builds its system session with the runtime instance The alerts-sink drain thread (and schedulers) run engine work through AlertsRuntime.asSystem with no QContext, so new QSystemUserSession() read a null instance and warned "QInstance was not supplied" on every call. AlertsSecurity.systemSession(QInstance) takes the instance; the no-arg form delegates with the context instance. Co-Authored-By: Claude Opus 5.5 --- .../kingsrook/qbits/alerts/AlertsRuntime.java | 2 +- .../qbits/alerts/AlertsSecurity.java | 14 ++++- .../qbits/alerts/AlertsSystemSessionTest.java | 56 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/kingsrook/qbits/alerts/AlertsSystemSessionTest.java diff --git a/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java b/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java index cf6d3b1..7fe44cb 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java +++ b/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java @@ -144,7 +144,7 @@ public class AlertsRuntime public R asSystem(SystemWork work) throws QException { Object[] result = new Object[1]; - QContext.withTemporaryContext(new CapturedContext(qInstance, AlertsSecurity.systemSession()), () -> + QContext.withTemporaryContext(new CapturedContext(qInstance, AlertsSecurity.systemSession(qInstance)), () -> { result[0] = work.run(); }); diff --git a/src/main/java/com/kingsrook/qbits/alerts/AlertsSecurity.java b/src/main/java/com/kingsrook/qbits/alerts/AlertsSecurity.java index 9abc04e..ca8a175 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/AlertsSecurity.java +++ b/src/main/java/com/kingsrook/qbits/alerts/AlertsSecurity.java @@ -278,7 +278,19 @@ public final class AlertsSecurity *******************************************************************************/ public static QSystemUserSession systemSession() { - QSystemUserSession session = new QSystemUserSession(); + return (systemSession(QContext.getQInstance())); + } + + + + /******************************************************************************* + ** A system-user session for this instance, holding the alerts all-access + ** keys. Background threads (the alerts sink, schedulers) have no QContext + ** yet, so they must pass the instance in. + *******************************************************************************/ + public static QSystemUserSession systemSession(QInstance qInstance) + { + QSystemUserSession session = new QSystemUserSession(qInstance); session.withSecurityKeyValue(KEY_USER_ALL_ACCESS, true); session.withSecurityKeyValue(KEY_PERMISSION_ALL, true); session.withSecurityKeyValue(KEY_READABLE_TABLE_ALL, true); diff --git a/src/test/java/com/kingsrook/qbits/alerts/AlertsSystemSessionTest.java b/src/test/java/com/kingsrook/qbits/alerts/AlertsSystemSessionTest.java new file mode 100644 index 0000000..3871d93 --- /dev/null +++ b/src/test/java/com/kingsrook/qbits/alerts/AlertsSystemSessionTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Kingsrook, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kingsrook.qbits.alerts; + + +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.logging.QCollectingLogger; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Background threads (the alerts sink, schedulers) run engine work through + ** asSystem with no QContext of their own; the system session must be built + ** from the runtime instance, not from the (empty) context. + *******************************************************************************/ +class AlertsSystemSessionTest extends BaseAlertsTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void asSystemWithoutContextUsesTheRuntimeInstance() throws Exception + { + QContext.clear(); + QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(QSystemUserSession.class); + try + { + Boolean systemUser = runtime.asSystem(() -> QContext.getQInstance() != null && AlertsSecurity.isSystemUser()); + assertThat(systemUser).isTrue(); + assertThat(collectingLogger.getCollectedMessages()) + .noneMatch(m -> m.getMessage().contains("QInstance was not supplied")); + } + finally + { + QLogger.deactivateCollectingLoggerForClass(QSystemUserSession.class); + } + } +} -- 2.54.0