using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests; /// /// M7-B4 (T15): searches its canned address-space /// tree (root → Folder1/Folder2; Folder1 → Tag1/Tag2) so DCL address-space /// search flows can be exercised without a live OPC UA server. The stub fakes /// BrowseNext continuation paging on Folder1, so finding both Tag1 and /// Tag2 also verifies that the BFS loops the continuation per node. /// public class StubOpcUaClientSearchTests { // StubOpcUaClient is internal; reached here via InternalsVisibleTo on the // DataConnectionLayer assembly (mirrors StubOpcUaClientBrowseTests). private static async Task ConnectedStubAsync() { var client = new StubOpcUaClient(); await client.ConnectAsync("opc.tcp://stub", null, CancellationToken.None); return client; } [Fact] public async Task Search_Tag_FindsBothTags_WithRootRelativePaths() { await using var client = await ConnectedStubAsync(); var result = await client.SearchAddressSpaceAsync("Tag", maxDepth: 5, maxResults: 50); Assert.False(result.CapReached); // Tag2 lives on page 2 of Folder1's children, so finding it proves the // BFS loops the stub's continuation token rather than stopping at page 1. var paths = result.Matches.Select(m => m.Path).OrderBy(p => p).ToArray(); Assert.Equal(new[] { "/Folder1/Tag1", "/Folder1/Tag2" }, paths); Assert.All(result.Matches, m => Assert.Contains("Tag", m.Node.DisplayName)); } [Fact] public async Task Search_Folder_MatchesOnDisplayNameAndPath() { await using var client = await ConnectedStubAsync(); var result = await client.SearchAddressSpaceAsync("Folder", maxDepth: 5, maxResults: 50); Assert.False(result.CapReached); // The contract matches DisplayName OR root-relative path. "Folder" hits // both folders by DisplayName AND both tags by path (they live under // /Folder1), so all four nodes match. var paths = result.Matches.Select(m => m.Path).OrderBy(p => p).ToArray(); Assert.Equal(new[] { "/Folder1", "/Folder1/Tag1", "/Folder1/Tag2", "/Folder2" }, paths); } [Fact] public async Task Search_Folder2_MatchesOnlyTheFolderByDisplayName() { await using var client = await ConnectedStubAsync(); // "Folder2" appears in no other node's DisplayName or path, so it isolates // a single match — proving the substring match is not over-broad. var result = await client.SearchAddressSpaceAsync("Folder2", maxDepth: 5, maxResults: 50); Assert.False(result.CapReached); var only = Assert.Single(result.Matches); Assert.Equal("/Folder2", only.Path); Assert.Equal("Folder2", only.Node.DisplayName); } [Fact] public async Task Search_IsCaseInsensitive() { await using var client = await ConnectedStubAsync(); var result = await client.SearchAddressSpaceAsync("tag", maxDepth: 5, maxResults: 50); Assert.Equal(2, result.Matches.Count); } [Fact] public async Task Search_MaxResultsOne_StopsEarly_WithCapReached() { await using var client = await ConnectedStubAsync(); var result = await client.SearchAddressSpaceAsync("Tag", maxDepth: 5, maxResults: 1); Assert.True(result.CapReached); var only = Assert.Single(result.Matches); Assert.Contains("Tag", only.Node.DisplayName); } [Fact] public async Task Search_EmptyQuery_ReturnsNoMatches() { await using var client = await ConnectedStubAsync(); var result = await client.SearchAddressSpaceAsync("", maxDepth: 5, maxResults: 50); Assert.Empty(result.Matches); Assert.False(result.CapReached); } [Fact] public async Task Search_WhitespaceQuery_ReturnsNoMatches() { await using var client = await ConnectedStubAsync(); var result = await client.SearchAddressSpaceAsync(" ", maxDepth: 5, maxResults: 50); Assert.Empty(result.Matches); Assert.False(result.CapReached); } }