Add Galaxy repository API and clients
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using MxGateway.Server.Galaxy;
|
||||
|
||||
namespace MxGateway.Tests.Galaxy;
|
||||
|
||||
public sealed class GalaxyDeployNotifierTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_NoLatestEvent_BlocksUntilPublish()
|
||||
{
|
||||
GalaxyDeployNotifier notifier = new();
|
||||
using CancellationTokenSource cts = new();
|
||||
|
||||
IAsyncEnumerator<GalaxyDeployEventInfo> enumerator = notifier
|
||||
.SubscribeAsync(cts.Token)
|
||||
.GetAsyncEnumerator(cts.Token);
|
||||
|
||||
ValueTask<bool> moveNext = enumerator.MoveNextAsync();
|
||||
Assert.False(moveNext.IsCompleted);
|
||||
|
||||
GalaxyDeployEventInfo published = new(
|
||||
Sequence: 1,
|
||||
ObservedAt: DateTimeOffset.UtcNow,
|
||||
TimeOfLastDeploy: DateTimeOffset.UtcNow,
|
||||
ObjectCount: 5,
|
||||
AttributeCount: 25);
|
||||
notifier.Publish(published);
|
||||
|
||||
Assert.True(await moveNext.AsTask().WaitAsync(TimeSpan.FromSeconds(1)));
|
||||
Assert.Same(published, enumerator.Current);
|
||||
|
||||
await cts.CancelAsync();
|
||||
await enumerator.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_WithLatestEvent_BootstrapsImmediately()
|
||||
{
|
||||
GalaxyDeployNotifier notifier = new();
|
||||
GalaxyDeployEventInfo first = new(1, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, 3, 9);
|
||||
notifier.Publish(first);
|
||||
|
||||
using CancellationTokenSource cts = new();
|
||||
await using IAsyncEnumerator<GalaxyDeployEventInfo> enumerator = notifier
|
||||
.SubscribeAsync(cts.Token)
|
||||
.GetAsyncEnumerator(cts.Token);
|
||||
|
||||
Assert.True(await enumerator.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1)));
|
||||
Assert.Same(first, enumerator.Current);
|
||||
|
||||
await cts.CancelAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_FansOutToAllSubscribers()
|
||||
{
|
||||
GalaxyDeployNotifier notifier = new();
|
||||
using CancellationTokenSource cts = new();
|
||||
|
||||
await using IAsyncEnumerator<GalaxyDeployEventInfo> a = notifier
|
||||
.SubscribeAsync(cts.Token)
|
||||
.GetAsyncEnumerator(cts.Token);
|
||||
await using IAsyncEnumerator<GalaxyDeployEventInfo> b = notifier
|
||||
.SubscribeAsync(cts.Token)
|
||||
.GetAsyncEnumerator(cts.Token);
|
||||
|
||||
GalaxyDeployEventInfo info = new(1, DateTimeOffset.UtcNow, null, 0, 0);
|
||||
notifier.Publish(info);
|
||||
|
||||
Assert.True(await a.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1)));
|
||||
Assert.True(await b.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1)));
|
||||
Assert.Same(info, a.Current);
|
||||
Assert.Same(info, b.Current);
|
||||
|
||||
await cts.CancelAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Latest_TracksMostRecentPublish()
|
||||
{
|
||||
GalaxyDeployNotifier notifier = new();
|
||||
Assert.Null(notifier.Latest);
|
||||
|
||||
GalaxyDeployEventInfo first = new(1, DateTimeOffset.UtcNow, null, 0, 0);
|
||||
GalaxyDeployEventInfo second = new(2, DateTimeOffset.UtcNow, null, 0, 0);
|
||||
notifier.Publish(first);
|
||||
notifier.Publish(second);
|
||||
|
||||
Assert.Same(second, notifier.Latest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using MxGateway.Server.Galaxy;
|
||||
|
||||
namespace MxGateway.Tests.Galaxy;
|
||||
|
||||
public sealed class GalaxyHierarchyCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void Current_BeforeAnyRefresh_ReturnsEmpty()
|
||||
{
|
||||
GalaxyDeployNotifier notifier = new();
|
||||
GalaxyHierarchyCache cache = CreateCache(notifier, new ManualTimeProvider());
|
||||
|
||||
GalaxyHierarchyCacheEntry entry = cache.Current;
|
||||
|
||||
Assert.Equal(GalaxyCacheStatus.Unknown, entry.Status);
|
||||
Assert.False(entry.HasData);
|
||||
Assert.Equal(0, entry.ObjectCount);
|
||||
Assert.Null(entry.Reply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshAsync_WhenSqlIsUnreachable_MarksUnavailableAndDoesNotPublish()
|
||||
{
|
||||
GalaxyDeployNotifier notifier = new();
|
||||
ManualTimeProvider clock = new(DateTimeOffset.Parse("2026-04-28T12:00:00Z"));
|
||||
GalaxyHierarchyCache cache = CreateCache(notifier, clock);
|
||||
|
||||
await cache.RefreshAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(GalaxyCacheStatus.Unavailable, cache.Current.Status);
|
||||
Assert.False(string.IsNullOrWhiteSpace(cache.Current.LastError));
|
||||
Assert.Null(notifier.Latest);
|
||||
Assert.True(cache.WaitForFirstLoadAsync(CancellationToken.None).IsCompletedSuccessfully);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasData_OnHealthyEntry_IsTrue()
|
||||
{
|
||||
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
|
||||
{
|
||||
Status = GalaxyCacheStatus.Healthy,
|
||||
LastSuccessAt = DateTimeOffset.UtcNow,
|
||||
ObjectCount = 1,
|
||||
};
|
||||
|
||||
Assert.True(entry.HasData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasData_OnUnknownEntry_IsFalse()
|
||||
{
|
||||
Assert.False(GalaxyHierarchyCacheEntry.Empty.HasData);
|
||||
}
|
||||
|
||||
private static GalaxyHierarchyCache CreateCache(GalaxyDeployNotifier notifier, TimeProvider clock)
|
||||
{
|
||||
GalaxyRepositoryOptions options = new()
|
||||
{
|
||||
ConnectionString = "Server=127.0.0.1,65500;Database=ZB;Connection Timeout=1;Encrypt=False;",
|
||||
CommandTimeoutSeconds = 1,
|
||||
};
|
||||
GalaxyRepository repository = new(options);
|
||||
return new GalaxyHierarchyCache(repository, notifier, clock);
|
||||
}
|
||||
|
||||
private sealed class ManualTimeProvider(DateTimeOffset start = default) : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now = start == default ? DateTimeOffset.UtcNow : start;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan duration) => _now += duration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using MxGateway.Server.Galaxy;
|
||||
using MxGateway.Server.Grpc;
|
||||
|
||||
namespace MxGateway.Tests.Galaxy;
|
||||
|
||||
public sealed class GalaxyProtoMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void MapAttribute_PreservesAllScalarFields()
|
||||
{
|
||||
GalaxyAttributeRow row = new()
|
||||
{
|
||||
GobjectId = 42,
|
||||
TagName = "Pump_001",
|
||||
AttributeName = "Speed",
|
||||
FullTagReference = "Pump_001.Speed",
|
||||
MxDataType = 3,
|
||||
DataTypeName = "Float",
|
||||
IsArray = false,
|
||||
ArrayDimension = null,
|
||||
MxAttributeCategory = 5,
|
||||
SecurityClassification = 2,
|
||||
IsHistorized = true,
|
||||
IsAlarm = false,
|
||||
};
|
||||
|
||||
GalaxyAttribute proto = GalaxyProtoMapper.MapAttribute(row);
|
||||
|
||||
Assert.Equal("Speed", proto.AttributeName);
|
||||
Assert.Equal("Pump_001.Speed", proto.FullTagReference);
|
||||
Assert.Equal(3, proto.MxDataType);
|
||||
Assert.Equal("Float", proto.DataTypeName);
|
||||
Assert.False(proto.IsArray);
|
||||
Assert.Equal(0, proto.ArrayDimension);
|
||||
Assert.False(proto.ArrayDimensionPresent);
|
||||
Assert.Equal(5, proto.MxAttributeCategory);
|
||||
Assert.Equal(2, proto.SecurityClassification);
|
||||
Assert.True(proto.IsHistorized);
|
||||
Assert.False(proto.IsAlarm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapAttribute_ArrayDimensionPresentFlag_DistinguishesNullFromZero()
|
||||
{
|
||||
GalaxyAttributeRow withDim = new() { ArrayDimension = 0, IsArray = true };
|
||||
GalaxyAttributeRow withoutDim = new() { ArrayDimension = null, IsArray = false };
|
||||
|
||||
Assert.True(GalaxyProtoMapper.MapAttribute(withDim).ArrayDimensionPresent);
|
||||
Assert.Equal(0, GalaxyProtoMapper.MapAttribute(withDim).ArrayDimension);
|
||||
|
||||
Assert.False(GalaxyProtoMapper.MapAttribute(withoutDim).ArrayDimensionPresent);
|
||||
Assert.Equal(0, GalaxyProtoMapper.MapAttribute(withoutDim).ArrayDimension);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapAttribute_NullDataTypeName_BecomesEmptyString()
|
||||
{
|
||||
GalaxyAttributeRow row = new() { DataTypeName = null };
|
||||
|
||||
GalaxyAttribute proto = GalaxyProtoMapper.MapAttribute(row);
|
||||
|
||||
Assert.Equal(string.Empty, proto.DataTypeName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapHierarchy_GroupsAttributesByGobjectId()
|
||||
{
|
||||
List<GalaxyHierarchyRow> hierarchy =
|
||||
[
|
||||
new() { GobjectId = 1, TagName = "A", BrowseName = "A", TemplateChain = ["RootTpl"] },
|
||||
new() { GobjectId = 2, TagName = "B", BrowseName = "B", ParentGobjectId = 1 },
|
||||
new() { GobjectId = 3, TagName = "C", BrowseName = "C", ParentGobjectId = 1 },
|
||||
];
|
||||
List<GalaxyAttributeRow> attributes =
|
||||
[
|
||||
new() { GobjectId = 1, AttributeName = "X", FullTagReference = "A.X" },
|
||||
new() { GobjectId = 2, AttributeName = "Y1", FullTagReference = "B.Y1" },
|
||||
new() { GobjectId = 2, AttributeName = "Y2", FullTagReference = "B.Y2" },
|
||||
];
|
||||
|
||||
List<GalaxyObject> result = GalaxyProtoMapper.MapHierarchy(hierarchy, attributes).ToList();
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Single(result[0].Attributes);
|
||||
Assert.Equal("X", result[0].Attributes[0].AttributeName);
|
||||
Assert.Equal(2, result[1].Attributes.Count);
|
||||
Assert.Empty(result[2].Attributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapObject_CopiesTemplateChain()
|
||||
{
|
||||
GalaxyHierarchyRow row = new()
|
||||
{
|
||||
GobjectId = 5,
|
||||
TagName = "Engine_001",
|
||||
ContainedName = "Engine",
|
||||
BrowseName = "Engine",
|
||||
TemplateChain = ["EngineTpl", "AppEngineBase"],
|
||||
};
|
||||
|
||||
GalaxyObject proto = GalaxyProtoMapper.MapObject(
|
||||
row,
|
||||
new Dictionary<int, List<GalaxyAttributeRow>>());
|
||||
|
||||
Assert.Equal(new[] { "EngineTpl", "AppEngineBase" }, proto.TemplateChain);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Options;
|
||||
using MxGateway.Contracts.Proto;
|
||||
using MxGateway.Server.Configuration;
|
||||
using MxGateway.Server.Dashboard;
|
||||
using MxGateway.Server.Galaxy;
|
||||
using MxGateway.Server.Metrics;
|
||||
using MxGateway.Server.Sessions;
|
||||
using MxGateway.Server.Workers;
|
||||
@@ -171,6 +172,51 @@ public sealed class DashboardSnapshotServiceTests
|
||||
Assert.Equal("session-newer", Assert.Single(snapshot.Faults).SessionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSnapshot_ProjectsGalaxySummaryFromHierarchyCache()
|
||||
{
|
||||
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
|
||||
{
|
||||
Status = GalaxyCacheStatus.Healthy,
|
||||
Sequence = 7,
|
||||
LastQueriedAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
||||
LastSuccessAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
||||
LastDeployTime = DateTimeOffset.Parse("2026-04-28T09:00:00Z"),
|
||||
Hierarchy =
|
||||
[
|
||||
new GalaxyHierarchyRow { GobjectId = 1, TagName = "Pump_001", BrowseName = "Pump_001", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] },
|
||||
new GalaxyHierarchyRow { GobjectId = 2, TagName = "Pump_002", BrowseName = "Pump_002", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] },
|
||||
new GalaxyHierarchyRow { GobjectId = 3, TagName = "Area_A", BrowseName = "Area_A", CategoryId = 13, IsArea = true, TemplateChain = ["$Area"] },
|
||||
],
|
||||
Attributes =
|
||||
[
|
||||
new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Speed", IsHistorized = true },
|
||||
new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Status", IsAlarm = true },
|
||||
],
|
||||
ObjectCount = 3,
|
||||
AreaCount = 1,
|
||||
AttributeCount = 2,
|
||||
HistorizedAttributeCount = 1,
|
||||
AlarmAttributeCount = 1,
|
||||
};
|
||||
using GatewayMetrics metrics = new();
|
||||
DashboardSnapshotService service = CreateService(
|
||||
new SessionRegistry(),
|
||||
metrics,
|
||||
galaxyHierarchyCache: new StubGalaxyHierarchyCache(entry));
|
||||
|
||||
DashboardSnapshot snapshot = service.GetSnapshot();
|
||||
|
||||
Assert.Equal(DashboardGalaxyStatus.Healthy, snapshot.Galaxy.Status);
|
||||
Assert.Equal(3, snapshot.Galaxy.ObjectCount);
|
||||
Assert.Equal(1, snapshot.Galaxy.AreaCount);
|
||||
Assert.Equal(2, snapshot.Galaxy.AttributeCount);
|
||||
Assert.Equal("$Pump", Assert.Single(snapshot.Galaxy.TopTemplates, t => t.TemplateName == "$Pump").TemplateName);
|
||||
Assert.Equal(2, snapshot.Galaxy.TopTemplates.First(t => t.TemplateName == "$Pump").InstanceCount);
|
||||
Assert.Contains(snapshot.Galaxy.ObjectCategories, c => c.CategoryName == "UserDefined" && c.ObjectCount == 2);
|
||||
Assert.Contains(snapshot.Galaxy.ObjectCategories, c => c.CategoryName == "Area" && c.ObjectCount == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WatchSnapshotsAsync_WhenSubscriberCancels_DisposesCleanly()
|
||||
{
|
||||
@@ -200,7 +246,8 @@ public sealed class DashboardSnapshotServiceTests
|
||||
private static DashboardSnapshotService CreateService(
|
||||
SessionRegistry registry,
|
||||
GatewayMetrics metrics,
|
||||
GatewayOptions? options = null)
|
||||
GatewayOptions? options = null,
|
||||
IGalaxyHierarchyCache? galaxyHierarchyCache = null)
|
||||
{
|
||||
GatewayOptions resolvedOptions = options ?? new GatewayOptions
|
||||
{
|
||||
@@ -215,9 +262,19 @@ public sealed class DashboardSnapshotServiceTests
|
||||
registry,
|
||||
metrics,
|
||||
configurationProvider,
|
||||
galaxyHierarchyCache ?? new StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry.Empty),
|
||||
Options.Create(resolvedOptions));
|
||||
}
|
||||
|
||||
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 static GatewaySession CreateSession(
|
||||
string sessionId,
|
||||
string? clientIdentity,
|
||||
|
||||
@@ -174,6 +174,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
|
||||
new MxAccessGrpcRequestValidator(),
|
||||
mapper,
|
||||
eventStreamService,
|
||||
_metrics,
|
||||
NullLogger<MxAccessGatewayService>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
@@ -5,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MxGateway.Contracts;
|
||||
using MxGateway.Contracts.Proto;
|
||||
using MxGateway.Server.Grpc;
|
||||
using MxGateway.Server.Metrics;
|
||||
using MxGateway.Server.Security.Authentication;
|
||||
using MxGateway.Server.Security.Authorization;
|
||||
using MxGateway.Server.Sessions;
|
||||
@@ -163,6 +165,50 @@ public sealed class MxAccessGatewayServiceTests
|
||||
Assert.Equal("session-1", sessionManager.LastReadEventsSessionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamEvents_WhenEventIsWritten_RecordsSendDuration()
|
||||
{
|
||||
using GatewayMetrics metrics = new();
|
||||
using MeterListener listener = new();
|
||||
List<string> families = [];
|
||||
listener.InstrumentPublished = (instrument, meterListener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == GatewayMetrics.MeterName
|
||||
&& instrument.Name == "mxgateway.events.stream_send.duration")
|
||||
{
|
||||
meterListener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
listener.SetMeasurementEventCallback<double>(
|
||||
(instrument, measurement, tags, _) =>
|
||||
{
|
||||
if (instrument.Name != "mxgateway.events.stream_send.duration")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, object?> tag in tags)
|
||||
{
|
||||
if (tag.Key == "family" && tag.Value is string family)
|
||||
{
|
||||
families.Add(family);
|
||||
}
|
||||
}
|
||||
});
|
||||
listener.Start();
|
||||
FakeSessionManager sessionManager = new();
|
||||
sessionManager.Events.Add(CreateWorkerEvent("session-1", workerSequence: 2));
|
||||
MxAccessGatewayService service = CreateService(sessionManager, metrics: metrics);
|
||||
TestServerStreamWriter<MxEvent> writer = new();
|
||||
|
||||
await service.StreamEvents(
|
||||
new StreamEventsRequest { SessionId = "session-1" },
|
||||
writer,
|
||||
new TestServerCallContext());
|
||||
|
||||
Assert.Equal([MxEventFamily.OnDataChange.ToString()], families);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseSession_WithBlankSessionId_ThrowsInvalidArgument()
|
||||
{
|
||||
@@ -178,7 +224,8 @@ public sealed class MxAccessGatewayServiceTests
|
||||
|
||||
private static MxAccessGatewayService CreateService(
|
||||
FakeSessionManager sessionManager,
|
||||
IGatewayRequestIdentityAccessor? identityAccessor = null)
|
||||
IGatewayRequestIdentityAccessor? identityAccessor = null,
|
||||
GatewayMetrics? metrics = null)
|
||||
{
|
||||
return new MxAccessGatewayService(
|
||||
sessionManager,
|
||||
@@ -186,6 +233,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
new MxAccessGrpcRequestValidator(),
|
||||
new MxAccessGrpcMapper(),
|
||||
new FakeEventStreamService(sessionManager),
|
||||
metrics ?? new GatewayMetrics(),
|
||||
NullLogger<MxAccessGatewayService>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class GatewayMetricsTests
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
metrics.SetWorkerEventQueueDepth(7);
|
||||
metrics.SetGrpcEventStreamQueueDepth(3);
|
||||
metrics.AdjustGrpcEventStreamQueueDepth(3);
|
||||
metrics.QueueOverflow("session-events");
|
||||
metrics.Fault("CommandTimeout");
|
||||
metrics.WorkerKilled("CommandTimeout");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MxGateway.Contracts.Proto;
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using MxGateway.Server.Security.Authorization;
|
||||
|
||||
namespace MxGateway.Tests.Security.Authorization;
|
||||
@@ -9,6 +10,9 @@ public sealed class GatewayGrpcScopeResolverTests
|
||||
[InlineData(typeof(OpenSessionRequest), GatewayScopes.SessionOpen)]
|
||||
[InlineData(typeof(CloseSessionRequest), GatewayScopes.SessionClose)]
|
||||
[InlineData(typeof(StreamEventsRequest), GatewayScopes.EventsRead)]
|
||||
[InlineData(typeof(TestConnectionRequest), GatewayScopes.MetadataRead)]
|
||||
[InlineData(typeof(GetLastDeployTimeRequest), GatewayScopes.MetadataRead)]
|
||||
[InlineData(typeof(DiscoverHierarchyRequest), GatewayScopes.MetadataRead)]
|
||||
public void ResolveRequiredScope_KnownRpcRequest_ReturnsExpectedScope(
|
||||
Type requestType,
|
||||
string expectedScope)
|
||||
|
||||
Reference in New Issue
Block a user