fix(mqtt-browser): reject a wildcard rebirth scope before any NCMD leaves

SparkplugRebirthScope (the AdminUI's typed input) rejects '+'/'#' and its own
remarks say it is 'advisory, not the gate' because MqttBrowseSession
.ResolveRebirthTargets 'refuses an unusable one BEFORE any message leaves'. That
claim was false: the bare {group}/{edgeNode} branch — honoured on purpose, since a
node that has not birthed is the prime rebirth target — gated on IsUsableLabel,
which checks length and control characters only. A scope of 'Plant1/#' therefore
published spBv1.0/Plant1/NCMD/# , a PUBLISH the MQTT spec forbids and a conformant
broker answers by closing the browse connection; '+' would have fanned one
operator click across every matching edge node.

The check is a separate IsUsableRebirthSegment rather than a tightening of
IsUsableLabel: that helper also judges Sparkplug METRIC names, which are arbitrary
publisher-chosen UTF-8 and never part of a topic, so tightening it there would drop
legitimate metrics from the tree. Cases 1 and 2 cannot reach this shape — their ids
come off SparkplugTopic.TryParse, which already rejects both characters.

Driver.Mqtt.Tests 724 (was 708).
This commit is contained in:
Joseph Doherty
2026-08-10 06:51:59 -04:00
parent 6eace4bbba
commit 3118213772
2 changed files with 42 additions and 1 deletions
@@ -854,6 +854,27 @@ public sealed class MqttBrowseSessionTests
s.PublishCountForTest.ShouldBe(0);
}
[Theory]
[InlineData("Plant1/#")]
[InlineData("Plant1/+")]
[InlineData("+/EdgeA")]
[InlineData("#/EdgeA")]
public async Task RequestRebirthAsync_WildcardScope_PublishesNothing(string scope)
{
// SparkplugRebirthScope (the AdminUI's typed input) rejects a wildcard and its own remarks say
// it is "advisory, not the gate" because ResolveRebirthTargets "refuses an unusable one BEFORE
// any message leaves". That claim has to be true here: an unobserved `{group}/{edgeNode}` scope
// is honoured on purpose (a node that has not birthed is the prime rebirth target), and the
// label check behind it looked only at length and control characters. A wildcard segment would
// therefore have published `spBv1.0/Plant1/NCMD/#` — a PUBLISH the MQTT spec forbids, which a
// conformant broker answers by closing the browse connection.
await using var s = new MqttBrowseSession(MqttMode.SparkplugB);
await Should.ThrowAsync<ArgumentException>(
async () => await s.RequestRebirthAsync(scope, TestContext.Current.CancellationToken));
s.PublishCountForTest.ShouldBe(0);
}
[Fact]
public async Task RequestRebirthAsync_InPlainMode_IsRefused()
{