Merge stream L (proto reserved/propagation comments + regen, stale docs, gateway test gaps)
This commit is contained in:
@@ -727,6 +727,10 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
|
||||
[pbr::OriginalName("ALARM_TRANSITION_KIND_RAISE")] Raise = 1,
|
||||
[pbr::OriginalName("ALARM_TRANSITION_KIND_ACKNOWLEDGE")] Acknowledge = 2,
|
||||
[pbr::OriginalName("ALARM_TRANSITION_KIND_CLEAR")] Clear = 3,
|
||||
/// <summary>
|
||||
/// Reserved: never produced by the gateway or worker. Retained so the numeric
|
||||
/// value is not reused.
|
||||
/// </summary>
|
||||
[pbr::OriginalName("ALARM_TRANSITION_KIND_RETRIGGER")] Retrigger = 4,
|
||||
}
|
||||
|
||||
@@ -12714,6 +12718,11 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
|
||||
/// <summary>Field number for the "operator_full_name" field.</summary>
|
||||
public const int OperatorFullNameFieldNumber = 6;
|
||||
private string operatorFullName_ = "";
|
||||
/// <summary>
|
||||
/// Accepted for wire compatibility but NOT propagated to MXAccess: the 8-arg
|
||||
/// AlarmAckByName that carried it returns -55. operator_domain IS used (the
|
||||
/// AuthenticateUser identity is "<operator_domain>\<operator_user>").
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public string OperatorFullName {
|
||||
@@ -13384,6 +13393,11 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
|
||||
/// <summary>Field number for the "operator_full_name" field.</summary>
|
||||
public const int OperatorFullNameFieldNumber = 8;
|
||||
private string operatorFullName_ = "";
|
||||
/// <summary>
|
||||
/// Accepted for wire compatibility but NOT propagated to MXAccess: the 8-arg
|
||||
/// AlarmAckByName that carried it returns -55. operator_domain IS used (the
|
||||
/// AuthenticateUser identity is "<operator_domain>\<operator_user>").
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public string OperatorFullName {
|
||||
@@ -17527,6 +17541,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
|
||||
/// <summary>Field number for the "correlation_id" field.</summary>
|
||||
public const int CorrelationIdFieldNumber = 2;
|
||||
private string correlationId_ = "";
|
||||
/// <summary>
|
||||
/// Echoes the request's client_correlation_id.
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public string CorrelationId {
|
||||
@@ -26125,7 +26142,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
|
||||
/// <summary>
|
||||
/// When the alarm originally entered the active state. Preserved across
|
||||
/// acknowledge transitions so the Part 9 condition keeps the original raise
|
||||
/// time. Unset on retrigger from a previously-cleared condition.
|
||||
/// time. Unset when the raise time is unknown (RETRIGGER is reserved and never produced).
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
|
||||
@@ -132,6 +132,12 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
|
||||
|
||||
public enum WorkerFaultCategory {
|
||||
[pbr::OriginalName("WORKER_FAULT_CATEGORY_UNSPECIFIED")] Unspecified = 0,
|
||||
/// <summary>
|
||||
/// Values 1 and 2 are reserved: never emitted. Bootstrap failures (bad
|
||||
/// arguments, gateway authentication) exit via WorkerExitCode before the pipe
|
||||
/// exists, so there is no channel to carry a fault. Names kept so the numbers
|
||||
/// are not reused.
|
||||
/// </summary>
|
||||
[pbr::OriginalName("WORKER_FAULT_CATEGORY_INVALID_ARGUMENTS")] InvalidArguments = 1,
|
||||
[pbr::OriginalName("WORKER_FAULT_CATEGORY_GATEWAY_AUTHENTICATION_FAILED")] GatewayAuthenticationFailed = 2,
|
||||
[pbr::OriginalName("WORKER_FAULT_CATEGORY_PROTOCOL_MISMATCH")] ProtocolMismatch = 3,
|
||||
|
||||
@@ -405,6 +405,9 @@ message AcknowledgeAlarmCommand {
|
||||
string operator_user = 3;
|
||||
string operator_node = 4;
|
||||
string operator_domain = 5;
|
||||
// Accepted for wire compatibility but NOT propagated to MXAccess: the 8-arg
|
||||
// AlarmAckByName that carried it returns -55. operator_domain IS used (the
|
||||
// AuthenticateUser identity is "<operator_domain>\<operator_user>").
|
||||
string operator_full_name = 6;
|
||||
// Operator credential used for AuthenticateUser before the AckMsg write.
|
||||
// Implementations must keep this field out of logs, metrics labels, command
|
||||
@@ -435,6 +438,9 @@ message AcknowledgeAlarmByNameCommand {
|
||||
string operator_user = 5;
|
||||
string operator_node = 6;
|
||||
string operator_domain = 7;
|
||||
// Accepted for wire compatibility but NOT propagated to MXAccess: the 8-arg
|
||||
// AlarmAckByName that carried it returns -55. operator_domain IS used (the
|
||||
// AuthenticateUser identity is "<operator_domain>\<operator_user>").
|
||||
string operator_full_name = 8;
|
||||
// Operator credential used for AuthenticateUser before the AckMsg write.
|
||||
// Implementations must keep this field out of logs, metrics labels, command
|
||||
@@ -549,6 +555,7 @@ message ShutdownWorkerCommand {
|
||||
|
||||
message MxCommandReply {
|
||||
string session_id = 1;
|
||||
// Echoes the request's client_correlation_id.
|
||||
string correlation_id = 2;
|
||||
MxCommandKind kind = 3;
|
||||
ProtocolStatus protocol_status = 4;
|
||||
@@ -880,7 +887,7 @@ message OnAlarmTransitionEvent {
|
||||
|
||||
// When the alarm originally entered the active state. Preserved across
|
||||
// acknowledge transitions so the Part 9 condition keeps the original raise
|
||||
// time. Unset on retrigger from a previously-cleared condition.
|
||||
// time. Unset when the raise time is unknown (RETRIGGER is reserved and never produced).
|
||||
google.protobuf.Timestamp original_raise_timestamp = 6;
|
||||
|
||||
// When this specific transition occurred (raise time on Raise, ack time on
|
||||
@@ -930,6 +937,8 @@ enum AlarmTransitionKind {
|
||||
ALARM_TRANSITION_KIND_RAISE = 1;
|
||||
ALARM_TRANSITION_KIND_ACKNOWLEDGE = 2;
|
||||
ALARM_TRANSITION_KIND_CLEAR = 3;
|
||||
// Reserved: never produced by the gateway or worker. Retained so the numeric
|
||||
// value is not reused.
|
||||
ALARM_TRANSITION_KIND_RETRIGGER = 4;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,10 @@ enum WorkerState {
|
||||
|
||||
enum WorkerFaultCategory {
|
||||
WORKER_FAULT_CATEGORY_UNSPECIFIED = 0;
|
||||
// Values 1 and 2 are reserved: never emitted. Bootstrap failures (bad
|
||||
// arguments, gateway authentication) exit via WorkerExitCode before the pipe
|
||||
// exists, so there is no channel to carry a fault. Names kept so the numbers
|
||||
// are not reused.
|
||||
WORKER_FAULT_CATEGORY_INVALID_ARGUMENTS = 1;
|
||||
WORKER_FAULT_CATEGORY_GATEWAY_AUTHENTICATION_FAILED = 2;
|
||||
WORKER_FAULT_CATEGORY_PROTOCOL_MISMATCH = 3;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using ZB.MOM.WW.Configuration;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Direct tests of the shared path-validation primitives so the rules are pinned independently of
|
||||
/// the options validators that call them.
|
||||
/// </summary>
|
||||
public sealed class GatewayConfigPathRulesTests
|
||||
{
|
||||
private const string Message = "path rule failed";
|
||||
|
||||
private static readonly string Root = Path.Combine(Path.GetTempPath(), "mxgw-app");
|
||||
|
||||
/// <summary>Verifies blank values are ignored by every rule.</summary>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void BlankValue_RecordsNoFailure(string? value)
|
||||
{
|
||||
ValidationBuilder builder = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfNotRooted(value, Message, builder);
|
||||
GatewayConfigPathRules.AddIfInvalidPath(value, Message, builder);
|
||||
GatewayConfigPathRules.AddIfUnderContentRoot(value, Root, Message, builder);
|
||||
|
||||
Assert.True(builder.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a relative path is rejected as not rooted.</summary>
|
||||
[Fact]
|
||||
public void AddIfNotRooted_RecordsFailure_ForRelativePath()
|
||||
{
|
||||
ValidationBuilder builder = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfNotRooted(Path.Combine("data", "auth.db"), Message, builder);
|
||||
|
||||
Assert.Equal([Message], builder.Failures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a rooted path passes.</summary>
|
||||
[Fact]
|
||||
public void AddIfNotRooted_Passes_ForRootedPath()
|
||||
{
|
||||
ValidationBuilder builder = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfNotRooted(Path.Combine(Root, "auth.db"), Message, builder);
|
||||
|
||||
Assert.True(builder.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a NUL character makes the path invalid.</summary>
|
||||
[Fact]
|
||||
public void AddIfInvalidPath_RecordsFailure_ForEmbeddedNul()
|
||||
{
|
||||
ValidationBuilder builder = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfInvalidPath("a\0b", Message, builder);
|
||||
|
||||
Assert.Equal([Message], builder.Failures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies an ordinary path is valid.</summary>
|
||||
[Fact]
|
||||
public void AddIfInvalidPath_Passes_ForOrdinaryPath()
|
||||
{
|
||||
ValidationBuilder builder = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfInvalidPath(Path.Combine(Root, "auth.db"), Message, builder);
|
||||
|
||||
Assert.True(builder.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies paths inside the content root, and the root itself, are rejected.</summary>
|
||||
[Fact]
|
||||
public void AddIfUnderContentRoot_RecordsFailure_ForPathInsideRoot()
|
||||
{
|
||||
ValidationBuilder inside = new();
|
||||
ValidationBuilder equal = new();
|
||||
ValidationBuilder trailing = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfUnderContentRoot(Path.Combine(Root, "sub", "auth.db"), Root, Message, inside);
|
||||
GatewayConfigPathRules.AddIfUnderContentRoot(Root, Root, Message, equal);
|
||||
GatewayConfigPathRules.AddIfUnderContentRoot(
|
||||
Path.Combine(Root, "auth.db"),
|
||||
Root + Path.DirectorySeparatorChar,
|
||||
Message,
|
||||
trailing);
|
||||
|
||||
Assert.Equal([Message], inside.Failures);
|
||||
Assert.Equal([Message], equal.Failures);
|
||||
Assert.Equal([Message], trailing.Failures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a sibling directory sharing the root's name prefix is not treated as inside it.</summary>
|
||||
[Fact]
|
||||
public void AddIfUnderContentRoot_Passes_ForSiblingWithSharedPrefix()
|
||||
{
|
||||
ValidationBuilder builder = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfUnderContentRoot(
|
||||
Root + "-data" + Path.DirectorySeparatorChar + "auth.db",
|
||||
Root,
|
||||
Message,
|
||||
builder);
|
||||
|
||||
Assert.True(builder.IsValid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a malformed value reports nothing here, leaving it to the path-validity rule.</summary>
|
||||
[Fact]
|
||||
public void AddIfUnderContentRoot_Passes_ForMalformedValue()
|
||||
{
|
||||
ValidationBuilder builder = new();
|
||||
|
||||
GatewayConfigPathRules.AddIfUnderContentRoot("a\0b", Root, Message, builder);
|
||||
|
||||
Assert.True(builder.IsValid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using ZB.MOM.WW.MxGateway.Server.Dashboard;
|
||||
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Dashboard;
|
||||
|
||||
/// <summary>Tests for <see cref="DashboardRedactor"/>.</summary>
|
||||
public sealed class DashboardRedactorTests
|
||||
{
|
||||
/// <summary>Verifies null, empty and whitespace values pass through unchanged.</summary>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Redact_ReturnsBlankValueUnchanged(string? value)
|
||||
{
|
||||
Assert.Equal(value, DashboardRedactor.Redact(value));
|
||||
}
|
||||
|
||||
/// <summary>Verifies harmless text is returned unchanged.</summary>
|
||||
[Fact]
|
||||
public void Redact_ReturnsOrdinaryTextUnchanged()
|
||||
{
|
||||
Assert.Equal("Session opened for Plant1", DashboardRedactor.Redact("Session opened for Plant1"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies text containing a sensitive marker is replaced whole, case-insensitively.</summary>
|
||||
[Theory]
|
||||
[InlineData("apikey=abc")]
|
||||
[InlineData("API_KEY abc")]
|
||||
[InlineData("Authorization: Basic abc")]
|
||||
[InlineData("user credential rejected")]
|
||||
[InlineData("bad Password supplied")]
|
||||
[InlineData("client SECRET")]
|
||||
[InlineData("hub Token minted")]
|
||||
public void Redact_ReplacesTextContainingSensitiveMarker(string value)
|
||||
{
|
||||
Assert.Equal(GatewayLogRedactor.RedactedValue, DashboardRedactor.Redact(value));
|
||||
}
|
||||
|
||||
/// <summary>Verifies a gateway API key goes through client-identity redaction and its secret never survives.</summary>
|
||||
[Fact]
|
||||
public void Redact_DelegatesGatewayKeysToClientIdentityRedaction()
|
||||
{
|
||||
const string Value = "Bearer mxgw_dev_topsecretvalue";
|
||||
|
||||
string? result = DashboardRedactor.Redact(Value);
|
||||
|
||||
Assert.Equal(GatewayLogRedactor.RedactClientIdentity(Value), result);
|
||||
Assert.DoesNotContain("topsecretvalue", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Threading.Channels;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Sessions;
|
||||
|
||||
/// <summary>Tests for the <see cref="EventSubscriberReplayAttachment"/> value shape.</summary>
|
||||
public sealed class EventSubscriberReplayAttachmentTests
|
||||
{
|
||||
/// <summary>Verifies the constructor exposes every component unchanged.</summary>
|
||||
[Fact]
|
||||
public void Constructor_ExposesComponents()
|
||||
{
|
||||
StubLease lease = new();
|
||||
MxEvent[] replay = [new MxEvent { WorkerSequence = 5 }, new MxEvent { WorkerSequence = 6 }];
|
||||
|
||||
EventSubscriberReplayAttachment attachment = new(lease, replay, true, 5, 6);
|
||||
|
||||
Assert.Same(lease, attachment.Lease);
|
||||
Assert.Same(replay, attachment.ReplayedEvents);
|
||||
Assert.True(attachment.Gap);
|
||||
Assert.Equal(5UL, attachment.OldestAvailableSequence);
|
||||
Assert.Equal(6UL, attachment.LiveResumeSequence);
|
||||
}
|
||||
|
||||
/// <summary>Verifies value equality holds for identical components and breaks when a watermark differs.</summary>
|
||||
[Fact]
|
||||
public void Equality_ComparesComponents()
|
||||
{
|
||||
StubLease lease = new();
|
||||
IReadOnlyList<MxEvent> replay = [];
|
||||
|
||||
EventSubscriberReplayAttachment a = new(lease, replay, false, 0, 10);
|
||||
EventSubscriberReplayAttachment same = new(lease, replay, false, 0, 10);
|
||||
EventSubscriberReplayAttachment other = a with { LiveResumeSequence = 11 };
|
||||
|
||||
Assert.Equal(a, same);
|
||||
Assert.NotEqual(a, other);
|
||||
}
|
||||
|
||||
/// <summary>Verifies disposing the lease through the attachment releases the subscriber.</summary>
|
||||
[Fact]
|
||||
public void Lease_DisposeIsObservableThroughAttachment()
|
||||
{
|
||||
StubLease lease = new();
|
||||
EventSubscriberReplayAttachment attachment = new(lease, [], false, 0, 0);
|
||||
|
||||
attachment.Lease.Dispose();
|
||||
|
||||
Assert.True(lease.Disposed);
|
||||
}
|
||||
|
||||
private sealed class StubLease : IEventSubscriberLease
|
||||
{
|
||||
private readonly Channel<MxEvent> channel = Channel.CreateUnbounded<MxEvent>();
|
||||
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
public ChannelReader<MxEvent> Reader => channel.Reader;
|
||||
|
||||
public void Dispose() => Disposed = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user