Merge branch 'fix/archreview-p2' into main (P2 tier: completeness & polish)
# Conflicts: # archreview/remediation/00-tracking.md # clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayCliSecretRedactor.cs # clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs # src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs
This commit is contained in:
@@ -223,6 +223,55 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal("session-fixture", request.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a reconnect-replay gap sentinel is surfaced as a typed
|
||||
/// <see cref="MxEventStreamItem"/> with the gap populated, while normal
|
||||
/// events pass through unchanged with IsReplayGap false.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StreamEventItemsAsync_SurfacesReplayGapSentinel()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddEvent(new MxEvent
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
ReplayGap = new ReplayGap
|
||||
{
|
||||
RequestedAfterSequence = 5,
|
||||
OldestAvailableSequence = 42,
|
||||
},
|
||||
});
|
||||
transport.AddEvent(new MxEvent
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Family = MxEventFamily.OnDataChange,
|
||||
WorkerSequence = 42,
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
List<MxEventStreamItem> items = [];
|
||||
await foreach (MxEventStreamItem item in session.StreamEventItemsAsync(afterWorkerSequence: 5))
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
Assert.Equal(2, items.Count);
|
||||
|
||||
MxEventStreamItem gap = items[0];
|
||||
Assert.True(gap.IsReplayGap);
|
||||
Assert.NotNull(gap.ReplayGap);
|
||||
Assert.Equal(5UL, gap.ReplayGap!.RequestedAfterSequence);
|
||||
Assert.Equal(42UL, gap.ReplayGap.OldestAvailableSequence);
|
||||
Assert.Same(gap.ReplayGap, gap.Event.ReplayGap);
|
||||
|
||||
MxEventStreamItem normal = items[1];
|
||||
Assert.False(normal.IsReplayGap);
|
||||
Assert.Null(normal.ReplayGap);
|
||||
Assert.Equal(42UL, normal.Event.WorkerSequence);
|
||||
Assert.Equal(MxEventFamily.OnDataChange, normal.Event.Family);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that close is explicit and idempotent.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -380,6 +429,297 @@ public sealed class MxGatewayClientSessionTests
|
||||
Assert.Equal(7, el.Value.Int32Value);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unregister builds an unregister command with the server handle.</summary>
|
||||
[Fact]
|
||||
public async Task UnregisterAsync_BuildsUnregisterCommand()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.Unregister,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
await session.UnregisterAsync(12);
|
||||
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.Unregister, request.Command.Kind);
|
||||
Assert.Equal(12, request.Command.Unregister.ServerHandle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that advise-supervisory builds the supervisory advise command.</summary>
|
||||
[Fact]
|
||||
public async Task AdviseSupervisoryAsync_BuildsAdviseSupervisoryCommand()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.AdviseSupervisory,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
await session.AdviseSupervisoryAsync(12, 34);
|
||||
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.AdviseSupervisory, request.Command.Kind);
|
||||
Assert.Equal(12, request.Command.AdviseSupervisory.ServerHandle);
|
||||
Assert.Equal(34, request.Command.AdviseSupervisory.ItemHandle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that add-buffered-item returns the item handle from the typed reply.</summary>
|
||||
[Fact]
|
||||
public async Task AddBufferedItemAsync_BuildsCommandAndReturnsItemHandle()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.AddBufferedItem,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
AddBufferedItem = new AddBufferedItemReply { ItemHandle = 77 },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
int itemHandle = await session.AddBufferedItemAsync(12, "Area001.Pump001.Speed", "runtime");
|
||||
|
||||
Assert.Equal(77, itemHandle);
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.AddBufferedItem, request.Command.Kind);
|
||||
Assert.Equal(12, request.Command.AddBufferedItem.ServerHandle);
|
||||
Assert.Equal("Area001.Pump001.Speed", request.Command.AddBufferedItem.ItemDefinition);
|
||||
Assert.Equal("runtime", request.Command.AddBufferedItem.ItemContext);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that set-buffered-update-interval builds the command with the interval.</summary>
|
||||
[Fact]
|
||||
public async Task SetBufferedUpdateIntervalAsync_BuildsCommandWithInterval()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.SetBufferedUpdateInterval,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
await session.SetBufferedUpdateIntervalAsync(12, 500);
|
||||
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.SetBufferedUpdateInterval, request.Command.Kind);
|
||||
Assert.Equal(12, request.Command.SetBufferedUpdateInterval.ServerHandle);
|
||||
Assert.Equal(500, request.Command.SetBufferedUpdateInterval.UpdateIntervalMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that suspend builds the command and returns the reply status.</summary>
|
||||
[Fact]
|
||||
public async Task SuspendAsync_BuildsCommandAndReturnsStatus()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.Suspend,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
Suspend = new SuspendReply { Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok } },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
MxStatusProxy? status = await session.SuspendAsync(12, 34);
|
||||
|
||||
Assert.NotNull(status);
|
||||
Assert.Equal(1, status!.Success);
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.Suspend, request.Command.Kind);
|
||||
Assert.Equal(12, request.Command.Suspend.ServerHandle);
|
||||
Assert.Equal(34, request.Command.Suspend.ItemHandle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that activate builds the command and returns the reply status.</summary>
|
||||
[Fact]
|
||||
public async Task ActivateAsync_BuildsCommandAndReturnsStatus()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.Activate,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
Activate = new ActivateReply { Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok } },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
MxStatusProxy? status = await session.ActivateAsync(12, 34);
|
||||
|
||||
Assert.NotNull(status);
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.Activate, request.Command.Kind);
|
||||
Assert.Equal(34, request.Command.Activate.ItemHandle);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write-secured builds a WriteSecured command with value and user ids.</summary>
|
||||
[Fact]
|
||||
public async Task WriteSecuredAsync_BuildsWriteSecuredCommand()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.WriteSecured,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
MxValue value = 123.ToMxValue();
|
||||
|
||||
await session.WriteSecuredAsync(12, 34, value, currentUserId: 5, verifierUserId: 6);
|
||||
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.WriteSecured, request.Command.Kind);
|
||||
Assert.Equal(12, request.Command.WriteSecured.ServerHandle);
|
||||
Assert.Equal(34, request.Command.WriteSecured.ItemHandle);
|
||||
Assert.Same(value, request.Command.WriteSecured.Value);
|
||||
Assert.Equal(5, request.Command.WriteSecured.CurrentUserId);
|
||||
Assert.Equal(6, request.Command.WriteSecured.VerifierUserId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MXAccess parity: WriteSecured issued before a prior AuthenticateUser fails
|
||||
/// natively. The client must surface that scripted failure (as an
|
||||
/// <see cref="MxAccessException"/>) rather than pre-validating it away.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteSecuredAsync_SurfacesNativeFailureWhenNotAuthenticated()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.WriteSecured,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure },
|
||||
Hresult = unchecked((int)0x80040200),
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
MxAccessException exception = await Assert.ThrowsAsync<MxAccessException>(
|
||||
async () => await session.WriteSecuredAsync(12, 34, 123.ToMxValue(), currentUserId: 0, verifierUserId: 0));
|
||||
|
||||
// The native HRESULT is surfaced; the request payload is never in the message.
|
||||
Assert.Contains("WriteSecured", exception.Message);
|
||||
Assert.Single(transport.InvokeCalls);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write-secured2 builds a WriteSecured2 command with value and timestamp.</summary>
|
||||
[Fact]
|
||||
public async Task WriteSecured2Async_BuildsWriteSecured2Command()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.WriteSecured2,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
MxValue value = 123.ToMxValue();
|
||||
MxValue timestampValue = DateTimeOffset.Parse("2026-01-01T00:00:00Z").ToMxValue();
|
||||
|
||||
await session.WriteSecured2Async(12, 34, value, timestampValue, currentUserId: 5, verifierUserId: 6);
|
||||
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.WriteSecured2, request.Command.Kind);
|
||||
Assert.Same(value, request.Command.WriteSecured2.Value);
|
||||
Assert.Same(timestampValue, request.Command.WriteSecured2.TimestampValue);
|
||||
Assert.Equal(5, request.Command.WriteSecured2.CurrentUserId);
|
||||
Assert.Equal(6, request.Command.WriteSecured2.VerifierUserId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that authenticate-user builds the command and returns the resolved user id.</summary>
|
||||
[Fact]
|
||||
public async Task AuthenticateUserAsync_BuildsCommandAndReturnsUserId()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.AuthenticateUser,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
AuthenticateUser = new AuthenticateUserReply { UserId = 4242 },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
int userId = await session.AuthenticateUserAsync(12, "operator", "s3cr3t-p@ss");
|
||||
|
||||
Assert.Equal(4242, userId);
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.AuthenticateUser, request.Command.Kind);
|
||||
Assert.Equal("operator", request.Command.AuthenticateUser.VerifyUser);
|
||||
Assert.Equal("s3cr3t-p@ss", request.Command.AuthenticateUser.VerifyUserPassword);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SECRET REDACTION: when AuthenticateUser fails, the surfaced exception message
|
||||
/// must never contain the credential — the error path is built only from
|
||||
/// reply-derived diagnostics, not the request payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AuthenticateUserAsync_FailureDoesNotLeakCredentialInErrorMessage()
|
||||
{
|
||||
const string password = "super-secret-credential-123";
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.AuthenticateUser,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure },
|
||||
Hresult = unchecked((int)0x80040210),
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
MxAccessException exception = await Assert.ThrowsAsync<MxAccessException>(
|
||||
async () => await session.AuthenticateUserAsync(12, "operator", password));
|
||||
|
||||
Assert.DoesNotContain(password, exception.Message);
|
||||
Assert.DoesNotContain(password, exception.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that archestra-user-to-id builds the command and returns the resolved user id.</summary>
|
||||
[Fact]
|
||||
public async Task ArchestraUserToIdAsync_BuildsCommandAndReturnsUserId()
|
||||
{
|
||||
FakeGatewayTransport transport = CreateTransport();
|
||||
transport.AddInvokeReply(new MxCommandReply
|
||||
{
|
||||
SessionId = "session-fixture",
|
||||
Kind = MxCommandKind.ArchestraUserToId,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
ArchestraUserToId = new ArchestrAUserToIdReply { UserId = 909 },
|
||||
});
|
||||
await using MxGatewayClient client = CreateClient(transport);
|
||||
MxGatewaySession session = await client.OpenSessionAsync();
|
||||
|
||||
int userId = await session.ArchestraUserToIdAsync(12, "BCC47053-9542-4D65-BDAA-BCDEA6A32A73");
|
||||
|
||||
Assert.Equal(909, userId);
|
||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
||||
Assert.Equal(MxCommandKind.ArchestraUserToId, request.Command.Kind);
|
||||
Assert.Equal("BCC47053-9542-4D65-BDAA-BCDEA6A32A73", request.Command.ArchestraUserToId.UserIdGuid);
|
||||
}
|
||||
|
||||
private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
|
||||
{
|
||||
return new MxGatewayClient(transport.Options, transport);
|
||||
|
||||
Reference in New Issue
Block a user