Compare commits

...
Author SHA1 Message Date
james.maesandClaude Opus 5.5 b638741c61 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 <noreply@anthropic.com>
2026-10-15 07:25:44 -05:00
james.maes 7f79f86222 Merge pull request #41 from QRun-IO/fix/producer-version-from-build
fix: qbit metadata version comes from the build
2026-10-15 01:42:57 -05:00
james.maesandClaude Opus 5.5 69b94a13f5 fix: the qbit metadata version comes from the build, not a hardcoded constant
The producer read a literal version, so released artifacts reported a stale
or -SNAPSHOT version. qbit-alerts.properties (version=${project.version}) is
Maven-filtered and the producer loads it; tests assert producer == bundle ==
artifact version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:23:05 -05:00
james.maes 884eab9432 Merge pull request #40 from QRun-IO/fix/help-bundle-version-test
test(help): bundle version test compares with the artifact version
2026-10-14 23:18:42 -05:00
9 changed files with 109 additions and 4 deletions

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ All notable changes to this project are documented here. The format follows Keep
## [0.1.0] - Unreleased
### Fixed
- Read the QBit metadata version from the Maven-filtered artifact version so release builds report their release version.
- Record-view child lists (e.g. a notification's Recipients) failed with "Error in JSON Serialization": the child table's meta-data includes the qbit config, and `AlertsQBitConfig.getRuntime()` points at a runtime that points back at the config, so Jackson recursed to its 1000-level nesting limit. `getRuntime()` and `getAudienceResolver()` are now `@JsonIgnore`, as in the other qbits' configs. A regression test renders and serializes every child-list widget.
### Added
+2
View File
@@ -160,6 +160,7 @@
<filtering>true</filtering>
<includes>
<include>help/*.yaml</include>
<include>qbit-alerts.properties</include>
</includes>
</resource>
<resource>
@@ -167,6 +168,7 @@
<filtering>false</filtering>
<excludes>
<exclude>help/*.yaml</exclude>
<exclude>qbit-alerts.properties</exclude>
</excludes>
</resource>
</resources>
@@ -17,8 +17,11 @@
package com.kingsrook.qbits.alerts;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import com.kingsrook.qbits.alerts.delivery.DeliveryWorker;
import com.kingsrook.qbits.alerts.engine.AlertsProcessListener;
import com.kingsrook.qbits.alerts.engine.AlertsSinkAdapter;
@@ -66,7 +69,7 @@ public class AlertsQBitProducer implements QBitMetaDataProducer<AlertsQBitConfig
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "qbit-alerts";
public static final String VERSION = "0.1.0";
public static final String VERSION = loadVersion();
private static final String JAVALIN_CLASS = "com.kingsrook.qqq.middleware.javalin.QJavalinMetaData";
@@ -75,6 +78,34 @@ public class AlertsQBitProducer implements QBitMetaDataProducer<AlertsQBitConfig
/*******************************************************************************
** Read the artifact version from the Maven-filtered resource.
*******************************************************************************/
private static String loadVersion()
{
try(InputStream inputStream = AlertsQBitProducer.class.getClassLoader().getResourceAsStream("qbit-alerts.properties"))
{
if(inputStream == null)
{
throw (new IllegalStateException("Missing qbit-alerts.properties; build with Maven to generate the artifact version"));
}
Properties properties = new Properties();
properties.load(inputStream);
String version = properties.getProperty("version");
if(version == null || version.isBlank() || version.contains("${"))
{
throw (new IllegalStateException("Missing or invalid version in qbit-alerts.properties; build with Maven resource filtering enabled"));
}
return (version);
}
catch(IOException e)
{
throw (new IllegalStateException("Could not read artifact version from qbit-alerts.properties", e));
}
}
/*******************************************************************************
** Fluent setter for the config.
*******************************************************************************/
@@ -144,7 +144,7 @@ public class AlertsRuntime
public <R> R asSystem(SystemWork<R> 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();
});
@@ -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);
@@ -0,0 +1 @@
version=${project.version}
@@ -64,6 +64,8 @@ class AlertsQBitProducerTest extends BaseAlertsTest
void testProduce()
{
assertThat(qInstance.getQBits()).hasSize(1);
assertThat(System.getProperty("qbit.artifactVersion")).isNotBlank().isEqualTo(AlertsQBitProducer.VERSION);
assertThat(qInstance.getQBits().values().iterator().next().getVersion()).isEqualTo(System.getProperty("qbit.artifactVersion"));
assertThat(qInstance.getQBits().values().iterator().next().getArtifactId()).isEqualTo(AlertsQBitProducer.ARTIFACT_ID);
assertThat(AlertsRuntime.find(qInstance)).isSameAs(runtime);
assertThat(AlertsRuntime.find(new QInstance())).isNull();
@@ -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);
}
}
}
@@ -158,7 +158,7 @@ class AlertsTablesGuardrailTest extends BaseAlertsTest
assertThat(HelpBundles.of(qInstance).getBundles()).singleElement().satisfies(bundle ->
{
assertThat(bundle.getId()).isEqualTo("qbit-alerts");
assertThat(bundle.getVersion()).as("the help bundle carries the artifact version").isEqualTo(System.getProperty("qbit.artifactVersion"));
assertThat(bundle.getVersion()).as("the help bundle carries the artifact and producer version").isEqualTo(System.getProperty("qbit.artifactVersion")).isEqualTo(AlertsQBitProducer.VERSION);
});
HelpBundles.syncOnStartup(qInstance);
}