using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using MxGateway.Contracts.Proto.Galaxy; using MxGateway.Server.Dashboard; using MxGateway.Server.Galaxy; using MxGateway.Server.Grpc; namespace MxGateway.Tests.Gateway.Grpc; public sealed class GalaxyRepositoryGrpcServiceTests { [Fact] public async Task DiscoverHierarchy_ReturnsRequestedPageAndTotals() { GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3))); DiscoverHierarchyReply reply = await service.DiscoverHierarchy( new DiscoverHierarchyRequest { PageSize = 2, }, new TestServerCallContext()); Assert.Equal(2, reply.Objects.Count); Assert.Equal("Object_001", reply.Objects[0].TagName); Assert.Equal("Object_002", reply.Objects[1].TagName); Assert.Equal("7:2", reply.NextPageToken); Assert.Equal(3, reply.TotalObjectCount); } [Fact] public async Task DiscoverHierarchy_WithNextPageToken_ReturnsRemainingObjects() { GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3))); DiscoverHierarchyReply reply = await service.DiscoverHierarchy( new DiscoverHierarchyRequest { PageSize = 2, PageToken = "7:2", }, new TestServerCallContext()); GalaxyObject item = Assert.Single(reply.Objects); Assert.Equal("Object_003", item.TagName); Assert.Equal("", reply.NextPageToken); Assert.Equal(3, reply.TotalObjectCount); } [Theory] [InlineData("-1", 1)] [InlineData("not-an-offset", 1)] [InlineData("7:4", 1)] [InlineData("6:2", 1)] [InlineData("", -1)] public async Task DiscoverHierarchy_WithInvalidPagingArguments_ReturnsInvalidArgument( string pageToken, int pageSize) { GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3))); RpcException exception = await Assert.ThrowsAsync( async () => await service.DiscoverHierarchy( new DiscoverHierarchyRequest { PageSize = pageSize, PageToken = pageToken, }, new TestServerCallContext())); Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); } private static GalaxyRepositoryGrpcService CreateService(GalaxyHierarchyCacheEntry entry) { GalaxyRepositoryOptions options = new() { ConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;", }; return new GalaxyRepositoryGrpcService( new global::MxGateway.Server.Galaxy.GalaxyRepository(options), new StubGalaxyHierarchyCache(entry), new GalaxyDeployNotifier(), NullLogger.Instance); } private static GalaxyHierarchyCacheEntry CreateEntry(IReadOnlyList objects) { return GalaxyHierarchyCacheEntry.Empty with { Status = GalaxyCacheStatus.Healthy, Sequence = 7, LastSuccessAt = DateTimeOffset.UtcNow, Objects = objects, DashboardSummary = DashboardGalaxySummary.Unknown with { Status = DashboardGalaxyStatus.Healthy, ObjectCount = objects.Count, }, ObjectCount = objects.Count, }; } private static IReadOnlyList CreateObjects(int count) { return Enumerable.Range(1, count) .Select(index => new GalaxyObject { GobjectId = index, TagName = $"Object_{index:000}", BrowseName = $"Object_{index:000}", }) .ToArray(); } private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache { public GalaxyHierarchyCacheEntry Current { get; } = current; public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask; } private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext { private readonly Metadata requestHeaders = []; private readonly Metadata responseTrailers = []; private readonly Dictionary userState = []; private Status status; private WriteOptions? writeOptions; protected override string MethodCore => "/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy"; protected override string HostCore => "localhost"; protected override string PeerCore => "ipv4:127.0.0.1:5000"; protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1); protected override Metadata RequestHeadersCore => requestHeaders; protected override CancellationToken CancellationTokenCore => cancellationToken; protected override Metadata ResponseTrailersCore => responseTrailers; protected override Status StatusCore { get => status; set => status = value; } protected override WriteOptions? WriteOptionsCore { get => writeOptions; set => writeOptions = value; } protected override AuthContext AuthContextCore { get; } = new( string.Empty, new Dictionary>(StringComparer.Ordinal)); protected override IDictionary UserStateCore => userState; protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) => Task.CompletedTask; protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options) { throw new NotSupportedException(); } } }