397d3c5c4f
Rename across every client surface using each language's idiomatic convention:
* .NET clients/dotnet/MxGateway.Client[.Cli|.Tests]/
-> clients/dotnet/ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]/
namespaces -> ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]
contracts ProjectReference repointed to ZB.MOM.WW.MxGateway.Contracts
sln migrated to slnx (dotnet sln migrate)
* Python src/mxgateway -> src/zb_mom_ww_mxgateway
src/mxgateway_cli -> src/zb_mom_ww_mxgateway_cli
distribution: mxaccess-gateway-client -> zb-mom-ww-mxaccess-gateway-client
* Rust crate: mxgateway-client -> zb-mom-ww-mxgateway-client
build.rs proto path repointed
* Java subprojects: mxgateway-{client,cli} -> zb-mom-ww-mxgateway-{client,cli}
packages com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
group com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
rootProject mxaccessgw-java -> zb-mom-ww-mxaccessgw-java
* Go generate-proto.ps1 proto path repointed; module path and
package mxgateway kept (Go convention).
* proto-inputs.json: generatedOutputs.python updated to new package path.
* scripts/run-client-e2e-tests.ps1: Java CLI install path + gradle task
updated to zb-mom-ww-mxgateway-cli.
CLI binary names (mxgw, mxgw-py, mxgw-go, mxgateway-cli) and wire-level
identifiers (MXGATEWAY_* env vars, the mxgw_<id>_<secret> API key
prefix, protobuf package names like mxaccess_gateway.v1, all MXAccess
references) intentionally NOT renamed.
Fix pre-existing alarms-over-gateway breaks unblocked by the rename:
* mxaccess_gateway.proto: add missing public message QueryActiveAlarmsRequest
{session_id, client_correlation_id, alarm_filter_prefix} and missing
rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns
(stream ActiveAlarmSnapshot). All four typed clients referenced
these but they were absent from the proto.
* MxAccessGatewayService.QueryActiveAlarms: implement the new RPC on
the server, streaming from IGatewayAlarmService.CurrentAlarms with
optional alarm_filter_prefix filter.
* clients/dotnet/.../DiscoverHierarchyOptions.cs: add the hand-written
.NET POCO that wraps DiscoverHierarchyRequest (referenced by
GalaxyRepositoryClient.DiscoverHierarchyAsync but never authored).
* Drop retired session_id field references from
AcknowledgeAlarmRequest/AcknowledgeAlarmReply test fixtures across
.NET, Rust, Go, and Python clients.
* Rust integration test: add the missing stream_alarms impl on the
fake MxAccessGateway server (the trait gained the method, fake
didn't).
* Rust CLI test: bump expected gatewayProtocolVersion 2 -> 3.
Regenerated artifacts updated in this commit:
* src/ZB.MOM.WW.MxGateway.Contracts/Generated/{MxaccessGateway,MxaccessGatewayGrpc}.cs
* clients/python/src/zb_mom_ww_mxgateway/generated/*_pb2{,_grpc}.py
* clients/go/internal/generated/*.pb.go
(C# regenerated by Grpc.Tools on contracts build; Python and Go via
their generate-proto.ps1 scripts; Rust regenerates from .proto via
tonic-build at compile time so no checked-in artefact.)
Verification: 472 server tests, 275 worker tests (9 dev-rig skipped),
18 integration tests (live MxAccess + LDAP + Galaxy), 57 .NET client
tests, 32 Rust workspace tests, 39 Python tests, all Go packages, and
gradle build for Java all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
189 lines
7.7 KiB
C#
189 lines
7.7 KiB
C#
using Google.Protobuf.WellKnownTypes;
|
|
using Grpc.Core;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Client.Tests;
|
|
|
|
/// <summary>
|
|
/// PR E.2 — pins the .NET SDK surface for the new alarm RPCs:
|
|
/// <see cref="MxGatewayClient.AcknowledgeAlarmAsync"/> and
|
|
/// <see cref="MxGatewayClient.QueryActiveAlarmsAsync"/>.
|
|
/// </summary>
|
|
public sealed class MxGatewayClientAlarmsTests
|
|
{
|
|
[Fact]
|
|
public async Task AcknowledgeAlarmAsync_RecordsRequestShapeAndReturnsReply()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddAcknowledgeReply(new AcknowledgeAlarmReply
|
|
{
|
|
CorrelationId = "corr-1",
|
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
Status = new MxStatusProxy
|
|
{
|
|
Success = 1,
|
|
Category = MxStatusCategory.Ok,
|
|
DetectedBy = MxStatusSource.RespondingLmx,
|
|
},
|
|
});
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
|
|
AcknowledgeAlarmReply reply = await client.AcknowledgeAlarmAsync(new AcknowledgeAlarmRequest
|
|
{
|
|
ClientCorrelationId = "corr-1",
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
Comment = "investigating",
|
|
OperatorUser = "alice",
|
|
});
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(MxStatusCategory.Ok, reply.Status.Category);
|
|
|
|
var call = Assert.Single(transport.AcknowledgeAlarmCalls);
|
|
Assert.Equal("Tank01.Level.HiHi", call.Request.AlarmFullReference);
|
|
Assert.Equal("investigating", call.Request.Comment);
|
|
Assert.Equal("alice", call.Request.OperatorUser);
|
|
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AcknowledgeAlarmAsync_HonorsCancellation()
|
|
{
|
|
// Acks are routed through the safe-unary retry pipeline (idempotent at the
|
|
// MxAccess level), so the transport-side cancellation token is a linked one
|
|
// rather than the caller's original. Verify cancellation by tripping the source
|
|
// and asserting the call observes it.
|
|
using CancellationTokenSource cancellation = new();
|
|
cancellation.Cancel();
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
|
client.AcknowledgeAlarmAsync(
|
|
new AcknowledgeAlarmRequest
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
Comment = string.Empty,
|
|
OperatorUser = "alice",
|
|
},
|
|
cancellation.Token));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AcknowledgeAlarmAsync_MapsUnauthenticated_RpcException_ToTypedException()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AcknowledgeAlarmExceptions.Enqueue(
|
|
new RpcException(new Status(StatusCode.Unauthenticated, "expired key")));
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
|
|
// Note: the FakeGatewayTransport surfaces RpcException directly (it does not run
|
|
// through GrpcMxGatewayClientTransport's mapping); the fake's contract here is to
|
|
// pass the exception verbatim. RpcException → typed exception mapping is covered
|
|
// in the GrpcMxGatewayClientTransport-level tests; the SDK-level test pins the
|
|
// pass-through shape so a future migration to direct mapping won't silently
|
|
// change observable behaviour.
|
|
var ex = await Assert.ThrowsAsync<RpcException>(
|
|
() => client.AcknowledgeAlarmAsync(new AcknowledgeAlarmRequest
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
Comment = string.Empty,
|
|
OperatorUser = "alice",
|
|
}));
|
|
Assert.Equal(StatusCode.Unauthenticated, ex.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QueryActiveAlarmsAsync_StreamsEnqueuedSnapshots()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank01.Level.HiHi", AlarmConditionState.Active));
|
|
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank02.Level.HiHi", AlarmConditionState.ActiveAcked));
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
|
|
List<ActiveAlarmSnapshot> snapshots = [];
|
|
await foreach (ActiveAlarmSnapshot snapshot in client.QueryActiveAlarmsAsync(new QueryActiveAlarmsRequest
|
|
{
|
|
SessionId = "session-fixture",
|
|
}))
|
|
{
|
|
snapshots.Add(snapshot);
|
|
}
|
|
|
|
Assert.Equal(2, snapshots.Count);
|
|
Assert.Equal("Tank01.Level.HiHi", snapshots[0].AlarmFullReference);
|
|
Assert.Equal(AlarmConditionState.Active, snapshots[0].CurrentState);
|
|
Assert.Equal(AlarmConditionState.ActiveAcked, snapshots[1].CurrentState);
|
|
Assert.Single(transport.QueryActiveAlarmsCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QueryActiveAlarmsAsync_PassesFilterPrefix()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
|
|
await foreach (ActiveAlarmSnapshot _ in client.QueryActiveAlarmsAsync(new QueryActiveAlarmsRequest
|
|
{
|
|
SessionId = "session-fixture",
|
|
AlarmFilterPrefix = "Tank01.",
|
|
}))
|
|
{
|
|
// no snapshots enqueued; just verifying the request passes through
|
|
}
|
|
|
|
var call = Assert.Single(transport.QueryActiveAlarmsCalls);
|
|
Assert.Equal("Tank01.", call.Request.AlarmFilterPrefix);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QueryActiveAlarmsAsync_HonorsCancellationDuringEnumeration()
|
|
{
|
|
FakeGatewayTransport transport = CreateTransport();
|
|
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank01.Level.HiHi", AlarmConditionState.Active));
|
|
transport.AddActiveAlarmSnapshot(MakeSnapshot("Tank02.Level.HiHi", AlarmConditionState.Active));
|
|
await using MxGatewayClient client = CreateClient(transport);
|
|
|
|
using CancellationTokenSource cancellation = new();
|
|
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
|
{
|
|
await foreach (ActiveAlarmSnapshot _ in client.QueryActiveAlarmsAsync(
|
|
new QueryActiveAlarmsRequest { SessionId = "session-fixture" },
|
|
cancellation.Token))
|
|
{
|
|
cancellation.Cancel();
|
|
}
|
|
});
|
|
}
|
|
|
|
private static ActiveAlarmSnapshot MakeSnapshot(string fullReference, AlarmConditionState state)
|
|
{
|
|
return new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = fullReference,
|
|
SourceObjectReference = fullReference.Split('.')[0],
|
|
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
|
Severity = 750,
|
|
CurrentState = state,
|
|
Category = "Process",
|
|
Description = "Tank high-high level",
|
|
OriginalRaiseTimestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc)),
|
|
LastTransitionTimestamp = Timestamp.FromDateTime(new DateTime(2026, 5, 1, 12, 0, 30, DateTimeKind.Utc)),
|
|
};
|
|
}
|
|
|
|
private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
|
|
{
|
|
return new MxGatewayClient(transport.Options, transport);
|
|
}
|
|
|
|
private static FakeGatewayTransport CreateTransport()
|
|
{
|
|
return new FakeGatewayTransport(new MxGatewayClientOptions
|
|
{
|
|
Endpoint = new Uri("http://localhost:5000"),
|
|
ApiKey = "test-api-key",
|
|
});
|
|
}
|
|
}
|