615b487a77
Adds missing <summary>/<param> XML docs across 99 server, worker, and test files so CommentChecker reports zero issues (TreatWarningsAsErrors needs the analyzer clean). Bundles in WIP dashboard work: NavSection extraction, MainLayout/site.css/js styling alignment, and DashboardOptions/Auth tweaks.
631 lines
25 KiB
C#
631 lines
25 KiB
C#
using System.Globalization;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
using ZB.MOM.WW.MxGateway.Server.Dashboard;
|
|
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
|
using ZB.MOM.WW.MxGateway.Server.Metrics;
|
|
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
|
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
|
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
|
using ZB.MOM.WW.MxGateway.Server.Workers;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
|
|
|
public sealed class DashboardSnapshotServiceTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies snapshot returns empty collections and healthy status when registry is empty.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSnapshot_WhenRegistryEmpty_ReturnsEmptyOperationalState()
|
|
{
|
|
using GatewayMetrics metrics = new();
|
|
DashboardSnapshotService service = CreateService(new SessionRegistry(), metrics);
|
|
|
|
DashboardSnapshot snapshot = service.GetSnapshot();
|
|
|
|
Assert.Empty(snapshot.Sessions);
|
|
Assert.Empty(snapshot.Workers);
|
|
Assert.Empty(snapshot.Faults);
|
|
Assert.Contains(snapshot.Metrics, metric => metric.Name == "mxgateway.sessions.open" && metric.Value == 0);
|
|
Assert.Equal("Healthy", snapshot.GatewayStatus);
|
|
Assert.NotNull(snapshot.Configuration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies snapshot projects active, faulted, and closed session states with worker and metrics data.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSnapshot_ProjectsActiveAndFaultedSessionsWorkersMetricsAndFaults()
|
|
{
|
|
SessionRegistry registry = new();
|
|
GatewaySession activeSession = CreateSession(
|
|
"session-active",
|
|
"client-one",
|
|
DateTimeOffset.Parse("2026-04-26T10:00:00Z", CultureInfo.InvariantCulture));
|
|
activeSession.AttachWorkerClient(new FakeWorkerClient("session-active", 1201, WorkerClientState.Ready));
|
|
activeSession.MarkReady();
|
|
GatewaySession faultedSession = CreateSession(
|
|
"session-faulted",
|
|
"client-two",
|
|
DateTimeOffset.Parse("2026-04-26T10:01:00Z", CultureInfo.InvariantCulture));
|
|
faultedSession.AttachWorkerClient(new FakeWorkerClient("session-faulted", 1202, WorkerClientState.Faulted));
|
|
faultedSession.MarkFaulted("worker pipe disconnected");
|
|
GatewaySession closedSession = CreateSession(
|
|
"session-closed",
|
|
"client-three",
|
|
DateTimeOffset.Parse("2026-04-26T09:59:00Z", CultureInfo.InvariantCulture));
|
|
closedSession.AttachWorkerClient(new FakeWorkerClient("session-closed", 1203, WorkerClientState.Closed));
|
|
closedSession.TransitionTo(SessionState.Closed);
|
|
registry.TryAdd(activeSession);
|
|
registry.TryAdd(faultedSession);
|
|
registry.TryAdd(closedSession);
|
|
using GatewayMetrics metrics = new();
|
|
metrics.SessionOpened();
|
|
metrics.SessionOpened();
|
|
metrics.CommandStarted("Register");
|
|
metrics.CommandFailed("Register", "WorkerFaulted", TimeSpan.FromMilliseconds(7));
|
|
metrics.EventReceived("session-active", "OnDataChange");
|
|
metrics.Fault("WorkerFaulted");
|
|
DashboardSnapshotService service = CreateService(registry, metrics);
|
|
|
|
DashboardSnapshot snapshot = service.GetSnapshot();
|
|
|
|
Assert.Equal(3, snapshot.Sessions.Count);
|
|
Assert.Equal("session-faulted", snapshot.Sessions[0].SessionId);
|
|
Assert.Equal(SessionState.Faulted, snapshot.Sessions[0].State);
|
|
DashboardSessionSummary activeSummary = Assert.Single(
|
|
snapshot.Sessions,
|
|
session => session.SessionId == "session-active");
|
|
Assert.Equal(1, activeSummary.EventsReceived);
|
|
Assert.Equal(2, snapshot.Workers.Count);
|
|
Assert.DoesNotContain(snapshot.Workers, worker => worker.SessionId == "session-closed");
|
|
Assert.Contains(snapshot.Metrics, metric => metric.Name == "mxgateway.commands.started" && metric.Value == 1);
|
|
Assert.Contains(
|
|
snapshot.Metrics,
|
|
metric => metric.Name == "mxgateway.events.received"
|
|
&& metric.Dimension == "OnDataChange"
|
|
&& metric.Value == 1);
|
|
DashboardFaultSummary fault = Assert.Single(snapshot.Faults);
|
|
Assert.Equal("Worker", fault.Source);
|
|
Assert.Equal("session-faulted", fault.SessionId);
|
|
Assert.Equal("worker pipe disconnected", fault.Message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies snapshot redacts sensitive values from client identity, session name, and fault messages.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSnapshot_RedactsSecretsFromSessionAndFaultFields()
|
|
{
|
|
SessionRegistry registry = new();
|
|
GatewaySession session = CreateSession(
|
|
"session-redacted",
|
|
"Bearer mxgw_admin_super-secret",
|
|
DateTimeOffset.Parse("2026-04-26T10:00:00Z", CultureInfo.InvariantCulture),
|
|
clientSessionName: "password=hunter2",
|
|
clientCorrelationId: "token=abc123");
|
|
session.MarkFaulted("secret=credential-value");
|
|
registry.TryAdd(session);
|
|
using GatewayMetrics metrics = new();
|
|
DashboardSnapshotService service = CreateService(registry, metrics);
|
|
|
|
DashboardSnapshot snapshot = service.GetSnapshot();
|
|
|
|
DashboardSessionSummary summary = Assert.Single(snapshot.Sessions);
|
|
Assert.Equal("Bearer mxgw_admin_[redacted]", summary.ClientIdentity);
|
|
Assert.Equal("[redacted]", summary.ClientSessionName);
|
|
Assert.Equal("[redacted]", summary.ClientCorrelationId);
|
|
Assert.Equal("[redacted]", summary.LastFault);
|
|
Assert.Equal("[redacted]", Assert.Single(snapshot.Faults).Message);
|
|
Assert.Equal("[redacted]", snapshot.Configuration.Authentication.PepperSecretName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies snapshot generation does not mutate session or worker client state.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSnapshot_DoesNotMutateSessionOrWorkerState()
|
|
{
|
|
SessionRegistry registry = new();
|
|
GatewaySession session = CreateSession(
|
|
"session-active",
|
|
"client-one",
|
|
DateTimeOffset.Parse("2026-04-26T10:00:00Z", CultureInfo.InvariantCulture));
|
|
FakeWorkerClient workerClient = new("session-active", 1201, WorkerClientState.Ready);
|
|
session.AttachWorkerClient(workerClient);
|
|
session.MarkReady();
|
|
registry.TryAdd(session);
|
|
using GatewayMetrics metrics = new();
|
|
DashboardSnapshotService service = CreateService(registry, metrics);
|
|
|
|
service.GetSnapshot();
|
|
service.GetSnapshot();
|
|
|
|
Assert.Equal(1, registry.ActiveCount);
|
|
Assert.Equal(SessionState.Ready, session.State);
|
|
Assert.Equal(WorkerClientState.Ready, workerClient.State);
|
|
Assert.Equal(0, workerClient.StartCount);
|
|
Assert.Equal(0, workerClient.ShutdownCount);
|
|
Assert.Equal(0, workerClient.KillCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies snapshot respects configured limits for recent sessions and faults.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSnapshot_AppliesRecentSessionAndFaultLimits()
|
|
{
|
|
SessionRegistry registry = new();
|
|
GatewaySession olderSession = CreateSession(
|
|
"session-older",
|
|
"client-one",
|
|
DateTimeOffset.Parse("2026-04-26T10:00:00Z", CultureInfo.InvariantCulture));
|
|
GatewaySession newerSession = CreateSession(
|
|
"session-newer",
|
|
"client-two",
|
|
DateTimeOffset.Parse("2026-04-26T10:01:00Z", CultureInfo.InvariantCulture));
|
|
olderSession.MarkFaulted("older fault");
|
|
newerSession.MarkFaulted("newer fault");
|
|
registry.TryAdd(olderSession);
|
|
registry.TryAdd(newerSession);
|
|
using GatewayMetrics metrics = new();
|
|
DashboardSnapshotService service = CreateService(
|
|
registry,
|
|
metrics,
|
|
new GatewayOptions
|
|
{
|
|
Dashboard = new DashboardOptions
|
|
{
|
|
SnapshotIntervalMilliseconds = 1,
|
|
RecentSessionLimit = 1,
|
|
RecentFaultLimit = 1,
|
|
},
|
|
});
|
|
|
|
DashboardSnapshot snapshot = service.GetSnapshot();
|
|
|
|
Assert.Equal("session-newer", Assert.Single(snapshot.Sessions).SessionId);
|
|
Assert.Equal("session-newer", Assert.Single(snapshot.Faults).SessionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies snapshot projects Galaxy hierarchy cache data including templates and categories.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSnapshot_ProjectsGalaxySummaryFromHierarchyCache()
|
|
{
|
|
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
|
|
{
|
|
Status = GalaxyCacheStatus.Healthy,
|
|
Sequence = 7,
|
|
LastQueriedAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z", CultureInfo.InvariantCulture),
|
|
LastSuccessAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z", CultureInfo.InvariantCulture),
|
|
LastDeployTime = DateTimeOffset.Parse("2026-04-28T09:00:00Z", CultureInfo.InvariantCulture),
|
|
DashboardSummary = new DashboardGalaxySummary(
|
|
DashboardGalaxyStatus.Healthy,
|
|
LastQueriedAt: DateTimeOffset.Parse("2026-04-28T11:30:00Z", CultureInfo.InvariantCulture),
|
|
LastSuccessAt: DateTimeOffset.Parse("2026-04-28T11:30:00Z", CultureInfo.InvariantCulture),
|
|
LastDeployTime: DateTimeOffset.Parse("2026-04-28T09:00:00Z", CultureInfo.InvariantCulture),
|
|
LastError: null,
|
|
ObjectCount: 3,
|
|
AreaCount: 1,
|
|
AttributeCount: 2,
|
|
HistorizedAttributeCount: 1,
|
|
AlarmAttributeCount: 1,
|
|
TopTemplates:
|
|
[
|
|
new DashboardGalaxyTemplateUsage("$Pump", 2),
|
|
new DashboardGalaxyTemplateUsage("$Area", 1),
|
|
],
|
|
ObjectCategories:
|
|
[
|
|
new DashboardGalaxyCategoryCount(10, "UserDefined", 2),
|
|
new DashboardGalaxyCategoryCount(13, "Area", 1),
|
|
]),
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies snapshot watcher cancels cleanly when subscriber cancels.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetSnapshot_DoesNotSynchronouslyListApiKeys()
|
|
{
|
|
using GatewayMetrics metrics = new();
|
|
CountingApiKeyAdminStore apiKeyAdminStore = new();
|
|
DashboardSnapshotService service = CreateService(
|
|
new SessionRegistry(),
|
|
metrics,
|
|
apiKeyAdminStore: apiKeyAdminStore);
|
|
|
|
DashboardSnapshot snapshot = service.GetSnapshot();
|
|
|
|
Assert.Empty(snapshot.ApiKeys);
|
|
Assert.Equal(0, apiKeyAdminStore.ListCount);
|
|
}
|
|
|
|
/// <summary>Verifies that snapshot service refreshes API key summaries before each snapshot.</summary>
|
|
[Fact]
|
|
public async Task WatchSnapshotsAsync_RefreshesApiKeySummariesBeforeSnapshot()
|
|
{
|
|
using GatewayMetrics metrics = new();
|
|
CountingApiKeyAdminStore apiKeyAdminStore = new(
|
|
new ApiKeyRecord(
|
|
KeyId: "operator01",
|
|
KeyPrefix: "mxgw_operator01",
|
|
SecretHash: [1, 2, 3],
|
|
DisplayName: "Operator",
|
|
Scopes: new HashSet<string>([GatewayScopes.MetadataRead], StringComparer.Ordinal),
|
|
Constraints: ApiKeyConstraints.Empty with
|
|
{
|
|
BrowseSubtrees = ["Area1/*"],
|
|
},
|
|
CreatedUtc: DateTimeOffset.Parse("2026-04-28T12:00:00Z", CultureInfo.InvariantCulture),
|
|
LastUsedUtc: null,
|
|
RevokedUtc: null));
|
|
DashboardSnapshotService service = CreateService(
|
|
new SessionRegistry(),
|
|
metrics,
|
|
apiKeyAdminStore: apiKeyAdminStore);
|
|
using CancellationTokenSource cancellation = new(TimeSpan.FromSeconds(2));
|
|
await using IAsyncEnumerator<DashboardSnapshot> enumerator = service
|
|
.WatchSnapshotsAsync(cancellation.Token)
|
|
.GetAsyncEnumerator(cancellation.Token);
|
|
|
|
Assert.True(await enumerator.MoveNextAsync());
|
|
DashboardSnapshot snapshot = enumerator.Current;
|
|
|
|
DashboardApiKeySummary key = Assert.Single(snapshot.ApiKeys);
|
|
Assert.Equal("operator01", key.KeyId);
|
|
Assert.Equal(["Area1/*"], key.Constraints.BrowseSubtrees);
|
|
Assert.Equal(1, apiKeyAdminStore.ListCount);
|
|
}
|
|
|
|
/// <summary>Verifies that snapshot service reuses previous summaries when API key refresh fails.</summary>
|
|
[Fact]
|
|
public async Task WatchSnapshotsAsync_WhenApiKeyRefreshFails_ReusesPreviousSummaries()
|
|
{
|
|
using GatewayMetrics metrics = new();
|
|
SequencedApiKeyAdminStore apiKeyAdminStore = new(
|
|
new ApiKeyRecord(
|
|
KeyId: "operator01",
|
|
KeyPrefix: "mxgw_operator01",
|
|
SecretHash: [1, 2, 3],
|
|
DisplayName: "Operator",
|
|
Scopes: new HashSet<string>([GatewayScopes.MetadataRead], StringComparer.Ordinal),
|
|
Constraints: ApiKeyConstraints.Empty,
|
|
CreatedUtc: DateTimeOffset.Parse("2026-04-28T12:00:00Z", CultureInfo.InvariantCulture),
|
|
LastUsedUtc: null,
|
|
RevokedUtc: null));
|
|
DashboardSnapshotService service = CreateService(
|
|
new SessionRegistry(),
|
|
metrics,
|
|
new GatewayOptions
|
|
{
|
|
Dashboard = new DashboardOptions
|
|
{
|
|
SnapshotIntervalMilliseconds = 1,
|
|
},
|
|
},
|
|
apiKeyAdminStore: apiKeyAdminStore);
|
|
using CancellationTokenSource cancellation = new(TimeSpan.FromSeconds(2));
|
|
await using IAsyncEnumerator<DashboardSnapshot> enumerator = service
|
|
.WatchSnapshotsAsync(cancellation.Token)
|
|
.GetAsyncEnumerator(cancellation.Token);
|
|
|
|
Assert.True(await enumerator.MoveNextAsync());
|
|
DashboardSnapshot first = enumerator.Current;
|
|
apiKeyAdminStore.FailNext = true;
|
|
|
|
Assert.True(await enumerator.MoveNextAsync());
|
|
DashboardSnapshot second = enumerator.Current;
|
|
|
|
Assert.Equal("operator01", Assert.Single(first.ApiKeys).KeyId);
|
|
Assert.Equal("operator01", Assert.Single(second.ApiKeys).KeyId);
|
|
Assert.Equal(2, apiKeyAdminStore.ListCount);
|
|
}
|
|
|
|
/// <summary>Verifies that snapshot service disposes cleanly when subscriber cancels.</summary>
|
|
[Fact]
|
|
public async Task WatchSnapshotsAsync_WhenSubscriberCancels_DisposesCleanly()
|
|
{
|
|
using GatewayMetrics metrics = new();
|
|
DashboardSnapshotService service = CreateService(
|
|
new SessionRegistry(),
|
|
metrics,
|
|
new GatewayOptions
|
|
{
|
|
Dashboard = new DashboardOptions
|
|
{
|
|
SnapshotIntervalMilliseconds = 1,
|
|
},
|
|
});
|
|
using CancellationTokenSource cancellation = new();
|
|
await using IAsyncEnumerator<DashboardSnapshot> enumerator = service
|
|
.WatchSnapshotsAsync(cancellation.Token)
|
|
.GetAsyncEnumerator();
|
|
|
|
Assert.True(await enumerator.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1)));
|
|
await cancellation.CancelAsync();
|
|
bool hasNext = await enumerator.MoveNextAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1));
|
|
|
|
Assert.False(hasNext);
|
|
}
|
|
|
|
private static DashboardSnapshotService CreateService(
|
|
SessionRegistry registry,
|
|
GatewayMetrics metrics,
|
|
GatewayOptions? options = null,
|
|
IGalaxyHierarchyCache? galaxyHierarchyCache = null,
|
|
IApiKeyAdminStore? apiKeyAdminStore = null)
|
|
{
|
|
GatewayOptions resolvedOptions = options ?? new GatewayOptions
|
|
{
|
|
Dashboard = new DashboardOptions
|
|
{
|
|
SnapshotIntervalMilliseconds = 1,
|
|
},
|
|
};
|
|
GatewayConfigurationProvider configurationProvider = new(Options.Create(resolvedOptions));
|
|
|
|
return new DashboardSnapshotService(
|
|
registry,
|
|
metrics,
|
|
configurationProvider,
|
|
galaxyHierarchyCache ?? new StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry.Empty),
|
|
apiKeyAdminStore ?? new FakeApiKeyAdminStore(),
|
|
Options.Create(resolvedOptions));
|
|
}
|
|
|
|
private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
|
|
{
|
|
/// <summary>
|
|
/// Gets the current Galaxy hierarchy cache entry.
|
|
/// </summary>
|
|
public GalaxyHierarchyCacheEntry Current { get; } = current;
|
|
|
|
/// <summary>
|
|
/// Refreshes the cache asynchronously.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Completed task.</returns>
|
|
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
/// <summary>
|
|
/// Waits for the first cache load asynchronously.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Completed task.</returns>
|
|
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
}
|
|
|
|
private class FakeApiKeyAdminStore : IApiKeyAdminStore
|
|
{
|
|
/// <inheritdoc />
|
|
public Task CreateAsync(ApiKeyCreateRequest request, CancellationToken cancellationToken)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual Task<IReadOnlyList<ApiKeyRecord>> ListAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<IReadOnlyList<ApiKeyRecord>>([]);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> RevokeAsync(
|
|
string keyId,
|
|
DateTimeOffset revokedUtc,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(false);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> RotateAsync(
|
|
string keyId,
|
|
byte[] secretHash,
|
|
DateTimeOffset rotatedUtc,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(false);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> DeleteAsync(string keyId, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(false);
|
|
}
|
|
}
|
|
|
|
private class CountingApiKeyAdminStore(params ApiKeyRecord[] records) : FakeApiKeyAdminStore
|
|
{
|
|
/// <summary>Gets the count of list operations performed.</summary>
|
|
public int ListCount { get; protected set; }
|
|
|
|
/// <inheritdoc />
|
|
public override Task<IReadOnlyList<ApiKeyRecord>> ListAsync(CancellationToken cancellationToken)
|
|
{
|
|
ListCount++;
|
|
return Task.FromResult<IReadOnlyList<ApiKeyRecord>>(records);
|
|
}
|
|
}
|
|
|
|
private sealed class SequencedApiKeyAdminStore(ApiKeyRecord record) : CountingApiKeyAdminStore(record)
|
|
{
|
|
/// <summary>Gets or sets a value indicating whether the next list operation should fail.</summary>
|
|
public bool FailNext { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public override Task<IReadOnlyList<ApiKeyRecord>> ListAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (FailNext)
|
|
{
|
|
FailNext = false;
|
|
ListCount++;
|
|
throw new InvalidOperationException("Simulated SQLite failure.");
|
|
}
|
|
|
|
return base.ListAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
private static GatewaySession CreateSession(
|
|
string sessionId,
|
|
string? clientIdentity,
|
|
DateTimeOffset openedAt,
|
|
string? clientSessionName = "test-session",
|
|
string? clientCorrelationId = "client-correlation")
|
|
{
|
|
return new GatewaySession(
|
|
sessionId,
|
|
"mxaccess",
|
|
$"mxaccess-gateway-1-{sessionId}",
|
|
"nonce",
|
|
clientIdentity,
|
|
clientSessionName,
|
|
clientCorrelationId,
|
|
TimeSpan.FromSeconds(30),
|
|
TimeSpan.FromSeconds(5),
|
|
TimeSpan.FromSeconds(5),
|
|
openedAt);
|
|
}
|
|
|
|
private sealed class FakeWorkerClient(
|
|
string sessionId,
|
|
int? processId,
|
|
WorkerClientState state) : IWorkerClient
|
|
{
|
|
/// <summary>
|
|
/// Gets the session identifier.
|
|
/// </summary>
|
|
public string SessionId { get; } = sessionId;
|
|
|
|
/// <summary>
|
|
/// Gets the process identifier.
|
|
/// </summary>
|
|
public int? ProcessId { get; } = processId;
|
|
|
|
/// <summary>
|
|
/// Gets the current worker client state.
|
|
/// </summary>
|
|
public WorkerClientState State { get; private set; } = state;
|
|
|
|
/// <summary>
|
|
/// Gets the timestamp of the last heartbeat.
|
|
/// </summary>
|
|
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.Parse("2026-04-26T10:02:00Z", CultureInfo.InvariantCulture);
|
|
|
|
/// <summary>
|
|
/// Gets the count of start invocations.
|
|
/// </summary>
|
|
public int StartCount { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of shutdown invocations.
|
|
/// </summary>
|
|
public int ShutdownCount { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of kill invocations.
|
|
/// </summary>
|
|
public int KillCount { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Starts the worker client asynchronously.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Completed task.</returns>
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
StartCount++;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes a worker command asynchronously.
|
|
/// </summary>
|
|
/// <param name="command">The command to invoke.</param>
|
|
/// <param name="timeout">Command timeout.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Command reply.</returns>
|
|
public Task<WorkerCommandReply> InvokeAsync(
|
|
WorkerCommand command,
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(new WorkerCommandReply());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads events from the worker asynchronously.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Async enumerable of worker events.</returns>
|
|
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
await Task.CompletedTask;
|
|
yield break;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shuts down the worker client asynchronously.
|
|
/// </summary>
|
|
/// <param name="timeout">Shutdown timeout.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Completed task.</returns>
|
|
public Task ShutdownAsync(
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ShutdownCount++;
|
|
State = WorkerClientState.Closed;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Terminates the worker client.
|
|
/// </summary>
|
|
/// <param name="reason">Reason for termination.</param>
|
|
public void Kill(string reason)
|
|
{
|
|
KillCount++;
|
|
State = WorkerClientState.Faulted;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases resources used by this worker client.
|
|
/// </summary>
|
|
/// <returns>Completed value task.</returns>
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
}
|
|
}
|