using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests; /// /// Pins the browse NodeId encoding. This codec is the load-bearing detail of the whole picker: a NodeId /// that mis-parses sends the operator to a different column than the one they clicked, and the resulting /// tag polls the wrong data forever with no error anywhere. SQL Server identifiers may legally contain /// . and | (inside a quoted identifier), so the round-trip has to hold for those too. /// public sealed class SqlBrowseNodeIdTests { [Fact] public void ForSchema_roundTrips() { var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForSchema("dbo")); reference.Kind.ShouldBe(SqlBrowseNodeKind.Schema); reference.Schema.ShouldBe("dbo"); reference.Table.ShouldBeNull(); reference.Column.ShouldBeNull(); } [Fact] public void ForTable_roundTrips() { var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForTable("dbo", "TagValues")); reference.Kind.ShouldBe(SqlBrowseNodeKind.Table); reference.Schema.ShouldBe("dbo"); reference.Table.ShouldBe("TagValues"); reference.Column.ShouldBeNull(); } [Fact] public void ForColumn_roundTrips() { var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForColumn("dbo", "TagValues", "num_value")); reference.Kind.ShouldBe(SqlBrowseNodeKind.Column); reference.Schema.ShouldBe("dbo"); reference.Table.ShouldBe("TagValues"); reference.Column.ShouldBe("num_value"); } /// /// The exact case the plan's schema.table|column sketch cannot express: main.a.b|c reads /// equally as schema main + table a.b and as schema main.a + table b. /// [Theory] [InlineData("a.b", "c", "d")] [InlineData("a", "b.c", "d")] [InlineData("a", "b", "c.d")] [InlineData("a|b", "c", "d")] [InlineData("a", "b|c", "d")] [InlineData("a", "b", "c|d")] [InlineData(@"a\b", "c", "d")] [InlineData("a", @"b\|c", "d")] [InlineData("a", "b", @"c\\d")] [InlineData("a.b|c", "d.e|f", "g.h|i")] [InlineData("schema", "table", "column")] [InlineData("x:y", "z:w", "q:r")] public void AwkwardIdentifiers_roundTripExactly(string schema, string table, string column) { var reference = SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForColumn(schema, table, column)); reference.Kind.ShouldBe(SqlBrowseNodeKind.Column); reference.Schema.ShouldBe(schema); reference.Table.ShouldBe(table); reference.Column.ShouldBe(column); } /// /// Two genuinely different columns must never collide on one NodeId. Under the plan's literal /// schema.table|column sketch both of these render as a.b|c. /// [Fact] public void DistinctReferences_neverCollide() { var nested = SqlBrowseNodeId.ForColumn("a.b", "t", "c"); var flat = SqlBrowseNodeId.ForColumn("a", "b.t", "c"); nested.ShouldNotBe(flat); SqlBrowseNodeId.Parse(nested).Schema.ShouldBe("a.b"); SqlBrowseNodeId.Parse(flat).Schema.ShouldBe("a"); } /// A table NodeId and a schema NodeId are never confusable, whatever the names contain. [Fact] public void KindsAreDistinguishable() { SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForSchema("a|b")).Kind.ShouldBe(SqlBrowseNodeKind.Schema); SqlBrowseNodeId.Parse(SqlBrowseNodeId.ForTable("a", "b")).Kind.ShouldBe(SqlBrowseNodeKind.Table); SqlBrowseNodeId.ForSchema("a|b").ShouldNotBe(SqlBrowseNodeId.ForTable("a", "b")); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData("main")] // no kind prefix [InlineData("bogus:main")] // unknown kind [InlineData("table:main")] // table kind with only one part [InlineData("schema:main|extra")] // schema kind with two parts [InlineData("column:a|b")] // column kind with two parts [InlineData("column:a|b|c|d")] // column kind with four parts [InlineData("schema:")] // empty part [InlineData("table:main|")] // empty trailing part [InlineData(@"schema:main\")] // dangling escape [InlineData(":main")] // empty kind public void MalformedNodeIds_areRejected(string? nodeId) { SqlBrowseNodeId.TryParse(nodeId, out _).ShouldBeFalse(); Should.Throw(() => SqlBrowseNodeId.Parse(nodeId)); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void EmptyIdentifiers_areRejectedAtEncode(string? identifier) { Should.Throw(() => SqlBrowseNodeId.ForSchema(identifier!)); Should.Throw(() => SqlBrowseNodeId.ForTable("main", identifier!)); Should.Throw(() => SqlBrowseNodeId.ForColumn("main", "t", identifier!)); } }