diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayConfigPathRulesTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayConfigPathRulesTests.cs
new file mode 100644
index 0000000..085d4ac
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayConfigPathRulesTests.cs
@@ -0,0 +1,122 @@
+using ZB.MOM.WW.Configuration;
+using ZB.MOM.WW.MxGateway.Server.Configuration;
+
+namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
+
+///
+/// Direct tests of the shared path-validation primitives so the rules are pinned independently of
+/// the options validators that call them.
+///
+public sealed class GatewayConfigPathRulesTests
+{
+ private const string Message = "path rule failed";
+
+ private static readonly string Root = Path.Combine(Path.GetTempPath(), "mxgw-app");
+
+ /// Verifies blank values are ignored by every rule.
+ [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);
+ }
+
+ /// Verifies a relative path is rejected as not rooted.
+ [Fact]
+ public void AddIfNotRooted_RecordsFailure_ForRelativePath()
+ {
+ ValidationBuilder builder = new();
+
+ GatewayConfigPathRules.AddIfNotRooted(Path.Combine("data", "auth.db"), Message, builder);
+
+ Assert.Equal([Message], builder.Failures);
+ }
+
+ /// Verifies a rooted path passes.
+ [Fact]
+ public void AddIfNotRooted_Passes_ForRootedPath()
+ {
+ ValidationBuilder builder = new();
+
+ GatewayConfigPathRules.AddIfNotRooted(Path.Combine(Root, "auth.db"), Message, builder);
+
+ Assert.True(builder.IsValid);
+ }
+
+ /// Verifies a NUL character makes the path invalid.
+ [Fact]
+ public void AddIfInvalidPath_RecordsFailure_ForEmbeddedNul()
+ {
+ ValidationBuilder builder = new();
+
+ GatewayConfigPathRules.AddIfInvalidPath("a\0b", Message, builder);
+
+ Assert.Equal([Message], builder.Failures);
+ }
+
+ /// Verifies an ordinary path is valid.
+ [Fact]
+ public void AddIfInvalidPath_Passes_ForOrdinaryPath()
+ {
+ ValidationBuilder builder = new();
+
+ GatewayConfigPathRules.AddIfInvalidPath(Path.Combine(Root, "auth.db"), Message, builder);
+
+ Assert.True(builder.IsValid);
+ }
+
+ /// Verifies paths inside the content root, and the root itself, are rejected.
+ [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);
+ }
+
+ /// Verifies a sibling directory sharing the root's name prefix is not treated as inside it.
+ [Fact]
+ public void AddIfUnderContentRoot_Passes_ForSiblingWithSharedPrefix()
+ {
+ ValidationBuilder builder = new();
+
+ GatewayConfigPathRules.AddIfUnderContentRoot(
+ Root + "-data" + Path.DirectorySeparatorChar + "auth.db",
+ Root,
+ Message,
+ builder);
+
+ Assert.True(builder.IsValid);
+ }
+
+ /// Verifies a malformed value reports nothing here, leaving it to the path-validity rule.
+ [Fact]
+ public void AddIfUnderContentRoot_Passes_ForMalformedValue()
+ {
+ ValidationBuilder builder = new();
+
+ GatewayConfigPathRules.AddIfUnderContentRoot("a\0b", Root, Message, builder);
+
+ Assert.True(builder.IsValid);
+ }
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Dashboard/DashboardRedactorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Dashboard/DashboardRedactorTests.cs
new file mode 100644
index 0000000..197486d
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Dashboard/DashboardRedactorTests.cs
@@ -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;
+
+/// Tests for .
+public sealed class DashboardRedactorTests
+{
+ /// Verifies null, empty and whitespace values pass through unchanged.
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Redact_ReturnsBlankValueUnchanged(string? value)
+ {
+ Assert.Equal(value, DashboardRedactor.Redact(value));
+ }
+
+ /// Verifies harmless text is returned unchanged.
+ [Fact]
+ public void Redact_ReturnsOrdinaryTextUnchanged()
+ {
+ Assert.Equal("Session opened for Plant1", DashboardRedactor.Redact("Session opened for Plant1"));
+ }
+
+ /// Verifies text containing a sensitive marker is replaced whole, case-insensitively.
+ [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));
+ }
+
+ /// Verifies a gateway API key goes through client-identity redaction and its secret never survives.
+ [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);
+ }
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Sessions/EventSubscriberReplayAttachmentTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Sessions/EventSubscriberReplayAttachmentTests.cs
new file mode 100644
index 0000000..47caabd
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Sessions/EventSubscriberReplayAttachmentTests.cs
@@ -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;
+
+/// Tests for the value shape.
+public sealed class EventSubscriberReplayAttachmentTests
+{
+ /// Verifies the constructor exposes every component unchanged.
+ [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);
+ }
+
+ /// Verifies value equality holds for identical components and breaks when a watermark differs.
+ [Fact]
+ public void Equality_ComparesComponents()
+ {
+ StubLease lease = new();
+ IReadOnlyList 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);
+ }
+
+ /// Verifies disposing the lease through the attachment releases the subscriber.
+ [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 channel = Channel.CreateUnbounded();
+
+ public bool Disposed { get; private set; }
+
+ public ChannelReader Reader => channel.Reader;
+
+ public void Dispose() => Disposed = true;
+ }
+}