291 lines
10 KiB
C#
291 lines
10 KiB
C#
using Microsoft.Extensions.Options;
|
|
using MxGateway.Contracts.Proto;
|
|
using MxGateway.Server.Configuration;
|
|
using MxGateway.Server.Dashboard;
|
|
using MxGateway.Server.Metrics;
|
|
using MxGateway.Server.Sessions;
|
|
using MxGateway.Server.Workers;
|
|
|
|
namespace MxGateway.Tests.Gateway.Dashboard;
|
|
|
|
public sealed class DashboardSnapshotServiceTests
|
|
{
|
|
[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);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSnapshot_ProjectsActiveAndFaultedSessionsWorkersMetricsAndFaults()
|
|
{
|
|
SessionRegistry registry = new();
|
|
GatewaySession activeSession = CreateSession(
|
|
"session-active",
|
|
"client-one",
|
|
DateTimeOffset.Parse("2026-04-26T10:00:00Z"));
|
|
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"));
|
|
faultedSession.AttachWorkerClient(new FakeWorkerClient("session-faulted", 1202, WorkerClientState.Faulted));
|
|
faultedSession.MarkFaulted("worker pipe disconnected");
|
|
registry.TryAdd(activeSession);
|
|
registry.TryAdd(faultedSession);
|
|
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(2, snapshot.Sessions.Count);
|
|
Assert.Equal("session-faulted", snapshot.Sessions[0].SessionId);
|
|
Assert.Equal(SessionState.Faulted, snapshot.Sessions[0].State);
|
|
Assert.Equal(2, snapshot.Workers.Count);
|
|
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);
|
|
}
|
|
|
|
[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"),
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSnapshot_DoesNotMutateSessionOrWorkerState()
|
|
{
|
|
SessionRegistry registry = new();
|
|
GatewaySession session = CreateSession(
|
|
"session-active",
|
|
"client-one",
|
|
DateTimeOffset.Parse("2026-04-26T10:00:00Z"));
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSnapshot_AppliesRecentSessionAndFaultLimits()
|
|
{
|
|
SessionRegistry registry = new();
|
|
GatewaySession olderSession = CreateSession(
|
|
"session-older",
|
|
"client-one",
|
|
DateTimeOffset.Parse("2026-04-26T10:00:00Z"));
|
|
GatewaySession newerSession = CreateSession(
|
|
"session-newer",
|
|
"client-two",
|
|
DateTimeOffset.Parse("2026-04-26T10:01:00Z"));
|
|
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);
|
|
}
|
|
|
|
[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)
|
|
{
|
|
GatewayOptions resolvedOptions = options ?? new GatewayOptions
|
|
{
|
|
Dashboard = new DashboardOptions
|
|
{
|
|
SnapshotIntervalMilliseconds = 1,
|
|
},
|
|
};
|
|
GatewayConfigurationProvider configurationProvider = new(Options.Create(resolvedOptions));
|
|
|
|
return new DashboardSnapshotService(
|
|
registry,
|
|
metrics,
|
|
configurationProvider,
|
|
Options.Create(resolvedOptions));
|
|
}
|
|
|
|
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
|
|
{
|
|
public string SessionId { get; } = sessionId;
|
|
|
|
public int? ProcessId { get; } = processId;
|
|
|
|
public WorkerClientState State { get; private set; } = state;
|
|
|
|
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.Parse("2026-04-26T10:02:00Z");
|
|
|
|
public int StartCount { get; private set; }
|
|
|
|
public int ShutdownCount { get; private set; }
|
|
|
|
public int KillCount { get; private set; }
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
StartCount++;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<WorkerCommandReply> InvokeAsync(
|
|
WorkerCommand command,
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(new WorkerCommandReply());
|
|
}
|
|
|
|
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
await Task.CompletedTask;
|
|
yield break;
|
|
}
|
|
|
|
public Task ShutdownAsync(
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ShutdownCount++;
|
|
State = WorkerClientState.Closed;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public void Kill(string reason)
|
|
{
|
|
KillCount++;
|
|
State = WorkerClientState.Faulted;
|
|
}
|
|
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
}
|
|
}
|