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"));
}