using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests; public sealed class AlarmEventIdTests { private static readonly DateTime T = new(2026, 10, 9, 1, 2, 3, DateTimeKind.Utc); private static AlarmHistorianEvent Ev(string path = "P", string id = "A", string kind = "Activated", string user = "u", string? comment = "c", string msg = "m", DateTime? t = null) => new(id, path, "n", "LimitAlarm", AlarmSeverity.High, kind, msg, user, comment, t ?? T); [Fact] public void Id_is_stable_lowercase_hex_sha256() { var a = AlarmEventIds.ComputeEventId(Ev()); a.ShouldBe(AlarmEventIds.ComputeEventId(Ev())); a.Length.ShouldBe(64); a.ShouldBe(a.ToLowerInvariant()); } [Fact] public void Field_boundaries_are_unambiguous() { // The delimiter-based scheme collided on these; length prefixes must not. AlarmEventIds.ComputeEventId(Ev(path: "a|b", id: "c")) .ShouldNotBe(AlarmEventIds.ComputeEventId(Ev(path: "a", id: "b|c"))); AlarmEventIds.ComputeEventId(Ev(path: "ab", id: "c")) .ShouldNotBe(AlarmEventIds.ComputeEventId(Ev(path: "a", id: "bc"))); } [Fact] public void Null_and_empty_differ_and_every_field_participates() { var basis = AlarmEventIds.ComputeEventId(Ev()); AlarmEventIds.ComputeEventId(Ev(comment: null)).ShouldNotBe(AlarmEventIds.ComputeEventId(Ev(comment: ""))); foreach (var other in new[] { Ev(path: "Q"), Ev(id: "B"), Ev(kind: "Cleared"), Ev(user: "v"), Ev(comment: "d"), Ev(msg: "x"), Ev(t: T.AddTicks(1)), }) AlarmEventIds.ComputeEventId(other).ShouldNotBe(basis); } [Theory] [InlineData(typeof(InvalidOperationException))] [InlineData(typeof(ArgumentException))] [InlineData(typeof(FormatException))] public void Non_SQL_exceptions_retry_rather_than_dead_letter(Type type) => SqlServerAlarmHistorianWriter.Classify((Exception)Activator.CreateInstance(type)!) .ShouldBe(HistorianWriteOutcome.RetryPlease); }