using Shouldly; using Xunit; using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Browse; /// /// Review 2026-10-21 G-5: Repository.DiscoverPageSize was authored and parsed but reached /// nothing — the client's typed DiscoverHierarchyAsync hard-codes 5000. The source now pages /// the raw RPC itself at the configured size. /// public sealed class GatewayGalaxyHierarchySourcePagingTests { private static GalaxyObject Obj(int id) => new() { GobjectId = id, TagName = $"Obj{id}" }; /// Every request carries the configured page size and the page tokens chain until the last page. [Fact] public async Task Pages_at_the_configured_size_and_chains_tokens() { var requests = new List(); Task Discover(DiscoverHierarchyRequest req, CancellationToken ct) { requests.Add(req); var reply = new DiscoverHierarchyReply(); switch (req.PageToken) { case "": reply.Objects.Add([Obj(1), Obj(2)]); reply.NextPageToken = "p2"; break; case "p2": reply.Objects.Add([Obj(3), Obj(4)]); reply.NextPageToken = "p3"; break; default: reply.Objects.Add(Obj(5)); reply.NextPageToken = ""; break; } return Task.FromResult(reply); } var objects = await GatewayGalaxyHierarchySource.PageAsync(Discover, pageSize: 2, CancellationToken.None); objects.Select(o => o.GobjectId).ShouldBe([1, 2, 3, 4, 5]); requests.Count.ShouldBe(3); requests.ShouldAllBe(r => r.PageSize == 2); requests.Select(r => r.PageToken).ShouldBe(["", "p2", "p3"]); } /// A different configured size is what goes on the wire (positive control for the knob). [Fact] public async Task A_different_page_size_is_sent_on_the_wire() { int? sent = null; Task Discover(DiscoverHierarchyRequest req, CancellationToken ct) { sent = req.PageSize; return Task.FromResult(new DiscoverHierarchyReply()); } await GatewayGalaxyHierarchySource.PageAsync(Discover, pageSize: 250, CancellationToken.None); sent.ShouldBe(250); } /// A non-positive configured size falls back to the client library's 5000 default. [Theory] [InlineData(0)] [InlineData(-7)] public void Non_positive_page_size_falls_back_to_the_default(int configured) => GatewayGalaxyHierarchySource.EffectivePageSize(configured).ShouldBe(GatewayGalaxyHierarchySource.DefaultPageSize); /// A repeated page token is refused rather than walked forever. [Fact] public async Task A_repeated_page_token_is_refused() { var calls = 0; Task Discover(DiscoverHierarchyRequest req, CancellationToken ct) { calls++; var reply = new DiscoverHierarchyReply { NextPageToken = "same" }; reply.Objects.Add(Obj(calls)); return Task.FromResult(reply); } var ex = await Should.ThrowAsync( () => GatewayGalaxyHierarchySource.PageAsync(Discover, pageSize: 10, CancellationToken.None)); ex.Message.ShouldContain("repeated page token"); calls.ShouldBe(2); } }