From d48674ba31062c456c2dbe9e3d72cb2b7232df35 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 06:05:58 -0400 Subject: [PATCH] =?UTF-8?q?fix(opcuaclient):=20review=20=E2=80=94=20UTC-ki?= =?UTF-8?q?nd=20the=20missing-time=20sentinel=20+=20test=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review I2: CoerceDateTime's missing-field sentinel was DateTime.MinValue (Kind=Unspecified) — a downstream .ToUniversalTime() could shift it; now UTC-kinded. M4: assert BrowsePath namespace==0 + the sentinel's UTC Kind. --- .../OpcUaClientDriver.cs | 6 +++++- .../OpcUaClientHistoryTests.cs | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs index bf9cb5ab..4dec9564 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs @@ -1751,8 +1751,12 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit _ => value.ToString(), }; + // Missing-field sentinel is UTC-kinded so a downstream .ToUniversalTime() can't shift it. + private static readonly DateTime MissingTimeSentinel = + DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); + private static DateTime CoerceDateTime(object? value) - => value is DateTime dt ? dt : DateTime.MinValue; + => value is DateTime dt ? dt : MissingTimeSentinel; private static ushort CoerceSeverity(object? value) { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs index 4207fa74..69eadae4 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs @@ -112,6 +112,7 @@ public sealed class OpcUaClientHistoryTests clause.AttributeId.ShouldBe(Attributes.Value); clause.BrowsePath.Count.ShouldBe(1); clause.BrowsePath[0].Name.ShouldBe(expected[i]); + clause.BrowsePath[0].NamespaceIndex.ShouldBe((ushort)0); // BaseEventType fields live in ns 0 } } @@ -165,6 +166,7 @@ public sealed class OpcUaClientHistoryTests mapped[0].EventId.ShouldBe(Convert.ToBase64String(new byte[] { 9 })); mapped[0].SourceName.ShouldBeNull(); mapped[0].EventTimeUtc.ShouldBe(DateTime.MinValue); + mapped[0].EventTimeUtc.Kind.ShouldBe(DateTimeKind.Utc); // sentinel is UTC-kinded, not Unspecified mapped[0].Severity.ShouldBe((ushort)0); } }