using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Browsing; /// /// Unit tests for — the operator-typed {group}/{edgeNode} /// rebirth scope that breaks the birth-driven-tree deadlock (Gitea #514). /// /// /// The rule is deliberately advisory — MqttBrowseSession.ResolveRebirthTargets re-resolves the /// scope and refuses an unusable one before publishing — so these tests pin the affordance /// behaviour: an operator must not be able to arm a two-click confirm that cannot succeed, and must /// not be blocked from a scope the session would accept. /// [Trait("Category", "Unit")] public sealed class SparkplugRebirthScopeTests { [Theory] [InlineData("OtOpcUaSim/EdgeA")] [InlineData(" OtOpcUaSim/EdgeA ")] [InlineData("g/e")] [InlineData("Plant 1/Edge Node 2")] // spaces are legal inside a Sparkplug id public void Accepts_a_group_and_edge_node(string scope) { SparkplugRebirthScope.Describe(scope).ShouldBeNull(); SparkplugRebirthScope.IsUsable(scope).ShouldBeTrue(); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Rejects_a_blank_scope(string? scope) => SparkplugRebirthScope.Describe(scope).ShouldNotBeNullOrWhiteSpace(); [Fact] public void Rejects_a_group_on_its_own() { // The session resolves a bare group by enumerating OBSERVED edge nodes, of which an empty tree // has none — so accepting it here would arm a confirm step that resolves to nothing, in exactly // the situation this input exists for. var error = SparkplugRebirthScope.Describe("OtOpcUaSim"); error.ShouldNotBeNullOrWhiteSpace(); error!.ShouldContain("group/edgeNode"); } [Theory] [InlineData("g/e/extra")] [InlineData("g//e")] [InlineData("/EdgeA")] [InlineData("OtOpcUaSim/")] public void Rejects_a_scope_that_is_not_exactly_two_segments(string scope) => SparkplugRebirthScope.Describe(scope).ShouldNotBeNullOrWhiteSpace(); [Theory] [InlineData("g/+")] [InlineData("#/e")] [InlineData("g/Edge#")] public void Rejects_an_mqtt_wildcard(string scope) { // A rebirth NCMD is addressed to one edge node on a literal topic; a wildcard names no single // node, and letting one through would build a topic the broker rejects. var error = SparkplugRebirthScope.Describe(scope); error.ShouldNotBeNullOrWhiteSpace(); error!.ShouldContain("wildcard"); } [Fact] public void Split_returns_the_trimmed_pair() { var (groupId, edgeNodeId) = SparkplugRebirthScope.Split(" OtOpcUaSim/EdgeA "); groupId.ShouldBe("OtOpcUaSim"); edgeNodeId.ShouldBe("EdgeA"); } [Fact] public void Split_refuses_a_scope_Describe_would_reject() => Should.Throw(() => SparkplugRebirthScope.Split("OtOpcUaSim")); }