d67634cc4f
The driver logged and surfaced the raw AgentUri (which may carry user:pass@) as HostName (the /hosts key), in ~20 log lines and in exception messages that reach DriverHealth.LastError, and the client's request-URI messages (timeouts, stream end, not-a-stream, framing) embedded it too - while the Test-Connect probe deliberately redacted it. New MTConnectUriRedaction (user-info, and for the configured string its query, removed; a clean URI is unchanged so host keys do not move); the driver computes its display form once per options document. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
126 lines
5.5 KiB
C#
126 lines
5.5 KiB
C#
using System.Net;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 R06:T-8 — the driver logged and surfaced the raw <c>AgentUri</c> (which may carry
|
|
/// <c>user:pass@</c>) as its <c>HostName</c>, in every log line and in exception messages that reach
|
|
/// <c>DriverHealth.LastError</c> → <c>/hosts</c>, while its own Test-Connect probe deliberately redacted it.
|
|
/// </summary>
|
|
public sealed class MTConnectUriRedactionTests
|
|
{
|
|
private const string Secret = "s3cretPw";
|
|
private const string CredentialedUri = "http://operator:" + Secret + "@fixture-agent:5000";
|
|
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
[Theory]
|
|
[InlineData("http://fixture-agent:5000", "http://fixture-agent:5000")] // unchanged when clean
|
|
[InlineData("http://agent:5000/prefix", "http://agent:5000/prefix")]
|
|
[InlineData("http://u:p@agent:5000", "http://agent:5000")]
|
|
[InlineData("https://u:p@agent/prefix?token=abc", "https://agent/prefix")]
|
|
[InlineData("not a uri with u:p@host", MTConnectUriRedaction.Unparseable)]
|
|
[InlineData("not-a-uri", "not-a-uri")]
|
|
public void The_configured_uri_loses_its_user_info(string raw, string expected) =>
|
|
MTConnectUriRedaction.Redact(raw).ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void A_request_uri_loses_its_user_info_and_keeps_its_query() =>
|
|
MTConnectUriRedaction.Redact(new Uri("http://u:p@agent:5000/sample?from=5&count=10"))
|
|
.ShouldBe("http://agent:5000/sample?from=5&count=10");
|
|
|
|
/// <summary>The host key, every log line and the health text of a credentialed driver carry no password.</summary>
|
|
[Fact]
|
|
public async Task A_credentialed_agent_uri_never_reaches_host_name_logs_or_health()
|
|
{
|
|
var logger = new RecordingDriverLogger();
|
|
var client = CannedAgentClient.FromFixtures();
|
|
var driver = new MTConnectDriver(
|
|
new MTConnectDriverOptions
|
|
{
|
|
AgentUri = CredentialedUri,
|
|
RequestTimeoutMs = 200,
|
|
Tags = [new MTConnectTagDefinition("dev1_pos", DriverDataType.Float64)],
|
|
},
|
|
"mt-t8",
|
|
_ => client,
|
|
logger);
|
|
await driver.InitializeAsync("{}", Ct);
|
|
try
|
|
{
|
|
// A /current that never answers: the read times out, degrades health and logs a Warning.
|
|
client.CurrentGate = new TaskCompletionSource();
|
|
await driver.ReadAsync(["dev1_pos"], Ct);
|
|
|
|
var health = driver.GetHealth();
|
|
health.State.ShouldBe(DriverState.Degraded, "positive control: the failure path that carries the URI ran");
|
|
health.LastError.ShouldNotBeNull().ShouldNotContain(Secret);
|
|
health.LastError.ShouldContain("fixture-agent:5000", Case.Sensitive, "the host is still named");
|
|
|
|
driver.HostName.ShouldBe("http://fixture-agent:5000");
|
|
((IHostConnectivityProbe)driver).GetHostStatuses().ShouldAllBe(s => !s.HostName.Contains(Secret));
|
|
|
|
var lines = logger.AllSnapshot();
|
|
lines.ShouldNotBeEmpty("positive control: the initialize line and the read warning were logged");
|
|
lines.ShouldContain(l => l.Contains("fixture-agent:5000"));
|
|
lines.ShouldAllBe(l => !l.Contains(Secret));
|
|
}
|
|
finally
|
|
{
|
|
client.CurrentGate?.TrySetResult();
|
|
await driver.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
/// <summary>The production client's own exception messages (they become health text) carry no password.</summary>
|
|
[Fact]
|
|
public async Task The_client_timeout_message_carries_no_password()
|
|
{
|
|
using var handler = new HangingHandler();
|
|
using var client = new MTConnectAgentClient(
|
|
new MTConnectDriverOptions { AgentUri = CredentialedUri, RequestTimeoutMs = 200 }, handler);
|
|
|
|
var ex = await Should.ThrowAsync<TimeoutException>(async () => await client.CurrentAsync(Ct));
|
|
|
|
ex.Message.ShouldContain("fixture-agent:5000");
|
|
ex.Message.ShouldNotContain(Secret);
|
|
}
|
|
|
|
/// <summary>The stream-ended exception (it becomes the pump's Degrade text) carries no password.</summary>
|
|
[Fact]
|
|
public async Task The_client_stream_messages_carry_no_password()
|
|
{
|
|
using var handler = new SingleDocumentHandler();
|
|
using var client = new MTConnectAgentClient(new MTConnectDriverOptions { AgentUri = CredentialedUri }, handler);
|
|
|
|
var ex = await Should.ThrowAsync<MTConnectStreamNotSupportedException>(async () =>
|
|
{
|
|
await foreach (var _ in client.SampleAsync(1, Ct)) { }
|
|
});
|
|
|
|
ex.Message.ShouldContain("fixture-agent:5000");
|
|
ex.Message.ShouldNotContain(Secret);
|
|
}
|
|
|
|
private sealed class HangingHandler : HttpMessageHandler
|
|
{
|
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
await Task.Delay(Timeout.Infinite, cancellationToken);
|
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
|
}
|
|
}
|
|
|
|
private sealed class SingleDocumentHandler : HttpMessageHandler
|
|
{
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
|
|
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(File.ReadAllText("Fixtures/current.xml"), System.Text.Encoding.UTF8, "text/xml"),
|
|
});
|
|
}
|
|
}
|