using System.Reflection; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.Historian; /// /// Structural contract tests for the historian data-source surface added in PR 1.1. /// Asserts the type shape — implementations are tested in their own projects. /// public sealed class IHistorianDataSourceContractTests { [Fact] public void Interface_LivesInRootNamespace() { typeof(IHistorianDataSource).Namespace .ShouldBe("ZB.MOM.WW.OtOpcUa.Core.Abstractions"); } [Fact] public void Interface_IsPublic() { typeof(IHistorianDataSource).IsPublic.ShouldBeTrue(); typeof(IHistorianDataSource).IsInterface.ShouldBeTrue(); } [Fact] public void Interface_ExtendsIDisposable() { typeof(IDisposable).IsAssignableFrom(typeof(IHistorianDataSource)) .ShouldBeTrue("data sources own backend connections; the server disposes them on shutdown"); } [Theory] [InlineData("ReadRawAsync", typeof(Task))] [InlineData("ReadProcessedAsync", typeof(Task))] [InlineData("ReadAtTimeAsync", typeof(Task))] [InlineData("ReadEventsAsync", typeof(Task))] public void ReadMethods_ReturnExpectedTaskShape(string methodName, Type expectedReturnType) { var method = typeof(IHistorianDataSource).GetMethod(methodName); method.ShouldNotBeNull(); method!.ReturnType.ShouldBe(expectedReturnType); } [Fact] public void GetHealthSnapshot_IsSynchronous() { var method = typeof(IHistorianDataSource).GetMethod("GetHealthSnapshot"); method.ShouldNotBeNull(); method!.ReturnType.ShouldBe(typeof(HistorianHealthSnapshot)); } [Fact] public void HealthSnapshot_AcceptsEmptyClusterNodeList() { var snapshot = new HistorianHealthSnapshot( TotalQueries: 0, TotalSuccesses: 0, TotalFailures: 0, ConsecutiveFailures: 0, LastSuccessTime: null, LastFailureTime: null, LastError: null, ProcessConnectionOpen: false, EventConnectionOpen: false, ActiveProcessNode: null, ActiveEventNode: null, Nodes: Array.Empty()); snapshot.Nodes.ShouldBeEmpty(); } [Fact] public void HealthSnapshot_PreservesClusterNodes() { var node = new HistorianClusterNodeState( Name: "hist-01", IsHealthy: true, CooldownUntil: null, FailureCount: 0, LastError: null, LastFailureTime: null); var snapshot = new HistorianHealthSnapshot( TotalQueries: 5, TotalSuccesses: 5, TotalFailures: 0, ConsecutiveFailures: 0, LastSuccessTime: new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Utc), LastFailureTime: null, LastError: null, ProcessConnectionOpen: true, EventConnectionOpen: true, ActiveProcessNode: "hist-01", ActiveEventNode: "hist-01", Nodes: new[] { node }); snapshot.Nodes.Count.ShouldBe(1); snapshot.Nodes[0].ShouldBe(node); } [Fact] public void ClusterNodeState_RecordEqualityByValue() { var a = new HistorianClusterNodeState("hist-01", true, null, 0, null, null); var b = new HistorianClusterNodeState("hist-01", true, null, 0, null, null); a.ShouldBe(b); } [Fact] public void ClusterNodeState_DistinctByAnyField() { var healthy = new HistorianClusterNodeState("hist-01", true, null, 0, null, null); var unhealthy = new HistorianClusterNodeState("hist-01", false, null, 1, "boom", null); healthy.ShouldNotBe(unhealthy); } }