test(server): cover GatewayConfigPathRules, DashboardRedactor, EventSubscriberReplayAttachment

This commit is contained in:
Joseph Doherty
2026-10-22 08:25:11 -04:00
parent dd3ea056b0
commit 908cf7ccca
3 changed files with 236 additions and 0 deletions
@@ -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;
}
}