using System.Net; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests; /// /// Review 2026-10-21 R06:T-8 — the driver logged and surfaced the raw AgentUri (which may carry /// user:pass@) as its HostName, in every log line and in exception messages that reach /// DriverHealth.LastError → /hosts, while its own Test-Connect probe deliberately redacted it. /// 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"); /// The host key, every log line and the health text of a credentialed driver carry no password. [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); } } /// The production client's own exception messages (they become health text) carry no password. [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(async () => await client.CurrentAsync(Ct)); ex.Message.ShouldContain("fixture-agent:5000"); ex.Message.ShouldNotContain(Secret); } /// The stream-ended exception (it becomes the pump's Degrade text) carries no password. [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(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 SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { await Task.Delay(Timeout.Infinite, cancellationToken); return new HttpResponseMessage(HttpStatusCode.OK); } } private sealed class SingleDocumentHandler : HttpMessageHandler { protected override Task 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"), }); } }