test: preserve server SDK 2.0 value security and history contracts

This commit is contained in:
Joseph Doherty
2026-11-10 10:37:52 -05:00
parent ac5e9d84f0
commit 9cddc9306b
45 changed files with 1215 additions and 641 deletions
@@ -41,19 +41,19 @@ public sealed class NodeManagerHistoryReadEventsProjectionTests : IDisposable
var history = Read(server, nm, notifier, Select("EventId", "EventType", "ConditionName", "SourceNode"), where: null);
var rows = history.Events.ToDictionary(e => System.Text.Encoding.UTF8.GetString((byte[])e.EventFields[0].Value));
rows["e-limit"].EventFields[1].Value.ShouldBe(ObjectTypeIds.LimitAlarmType);
rows["e-limit"].EventFields[2].Value.ShouldBe("HighTemp");
rows["e-limit"].EventFields[3].Value.ShouldBe(condition, "the alarm id resolves to the materialised condition — the live events' SourceNode");
var rows = history.Events.ToArray()!.ToDictionary(e => System.Text.Encoding.UTF8.GetString((byte[])SdkValueBoundary.ToClr(e.EventFields[0])!));
SdkValueBoundary.ToClr(rows["e-limit"].EventFields[1])!.ShouldBe(ObjectTypeIds.LimitAlarmType);
SdkValueBoundary.ToClr(rows["e-limit"].EventFields[2])!.ShouldBe("HighTemp");
SdkValueBoundary.ToClr(rows["e-limit"].EventFields[3])!.ShouldBe(condition, "the alarm id resolves to the materialised condition — the live events' SourceNode");
rows["e-offnormal"].EventFields[1].Value.ShouldBe(ObjectTypeIds.OffNormalAlarmType);
rows["e-offnormal"].EventFields[3].Value.ShouldBeNull("no condition is materialised for that alarm id");
SdkValueBoundary.ToClr(rows["e-offnormal"].EventFields[1])!.ShouldBe(ObjectTypeIds.OffNormalAlarmType);
SdkValueBoundary.ToClr(rows["e-offnormal"].EventFields[3])!.ShouldBeNull("no condition is materialised for that alarm id");
// No stored type: still an alarm condition (every historical row is alarm history); an unrecognised or non-event
// type name is not passed through as a non-event type.
rows["e-untyped"].EventFields[1].Value.ShouldBe(ObjectTypeIds.AlarmConditionType);
rows["e-untyped"].EventFields[2].Value.ShouldBeNull();
rows["e-bogus"].EventFields[1].Value.ShouldBe(ObjectTypeIds.AlarmConditionType);
SdkValueBoundary.ToClr(rows["e-untyped"].EventFields[1])!.ShouldBe(ObjectTypeIds.AlarmConditionType);
SdkValueBoundary.ToClr(rows["e-untyped"].EventFields[2])!.ShouldBeNull();
SdkValueBoundary.ToClr(rows["e-bogus"].EventFields[1])!.ShouldBe(ObjectTypeIds.AlarmConditionType);
await host.DisposeAsync();
}
@@ -79,9 +79,7 @@ public sealed class NodeManagerHistoryReadEventsProjectionTests : IDisposable
{
var (host, server, nm, notifier) = await BootAsync();
var where = new ContentFilter();
where.Push(FilterOperator.GreaterThan,
new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("Severity")) { AttributeId = Attributes.Value },
new LiteralOperand(new Variant((ushort)600)));
where.Push(FilterOperator.GreaterThan, Variant.FromStructure(new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("Severity")) { AttributeId = Attributes.Value }), Variant.FromStructure(new LiteralOperand(new Variant((ushort)600))));
Ids(Read(server, nm, notifier, Select("EventId"), where)).ShouldBe(["e-limit", "e-bogus"]);
await host.DisposeAsync();
@@ -100,13 +98,13 @@ public sealed class NodeManagerHistoryReadEventsProjectionTests : IDisposable
switch (fault)
{
case "unknown operator":
where.Push((FilterOperator)99, new LiteralOperand(new Variant(1)), new LiteralOperand(new Variant(1)));
where.Push((FilterOperator)99, Variant.FromStructure(new LiteralOperand(new Variant(1))), Variant.FromStructure(new LiteralOperand(new Variant(1))));
break;
case "self-referencing element":
where.Push(FilterOperator.Not, new ElementOperand(0));
where.Push(FilterOperator.Not, Variant.FromStructure(new ElementOperand(0)));
break;
default:
where.Push(FilterOperator.Equals, new LiteralOperand(new Variant(1)));
where.Push(FilterOperator.Equals, Variant.FromStructure(new LiteralOperand(new Variant(1))));
break;
}
@@ -115,39 +113,39 @@ public sealed class NodeManagerHistoryReadEventsProjectionTests : IDisposable
// Positive control: the same read with a valid where-clause is served.
var valid = new ContentFilter();
valid.Push(FilterOperator.Equals, new LiteralOperand(new Variant(1)), new LiteralOperand(new Variant(1)));
valid.Push(FilterOperator.Equals, Variant.FromStructure(new LiteralOperand(new Variant(1))), Variant.FromStructure(new LiteralOperand(new Variant(1))));
StatusCode.IsGood(ReadStatus(server, nm, notifier, Select("EventId"), valid).Status).ShouldBeTrue();
await host.DisposeAsync();
}
private static string[] Ids(HistoryEvent h) =>
h.Events.Select(e => System.Text.Encoding.UTF8.GetString((byte[])e.EventFields[0].Value)).ToArray();
h.Events.ToArray()!.Select(e => System.Text.Encoding.UTF8.GetString((byte[])SdkValueBoundary.ToClr(e.EventFields[0])!)).ToArray();
private static ContentFilter OfType(NodeId type)
{
var f = new ContentFilter();
f.Push(FilterOperator.OfType, new LiteralOperand(new Variant(type)));
f.Push(FilterOperator.OfType, Variant.FromStructure(new LiteralOperand(new Variant(type))));
return f;
}
private static SimpleAttributeOperandCollection Select(params string[] leaves)
private static List<SimpleAttributeOperand> Select(params string[] leaves)
{
var c = new SimpleAttributeOperandCollection();
var c = new List<SimpleAttributeOperand>();
foreach (var leaf in leaves)
c.Add(new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName(leaf)) { AttributeId = Attributes.Value });
return c;
}
private static HistoryEvent Read(OtOpcUaSdkServer server, OtOpcUaNodeManager nm, NodeId notifier,
SimpleAttributeOperandCollection select, ContentFilter? where)
List<SimpleAttributeOperand> select, ContentFilter? where)
{
var (status, result) = ReadStatus(server, nm, notifier, select, where);
StatusCode.IsGood(status).ShouldBeTrue($"HistoryRead failed: {status}");
return (HistoryEvent)ExtensionObject.ToEncodeable(result.HistoryData);
return (HistoryEvent)ExtensionObject.ToEncodeable(result.HistoryData)!;
}
private static (StatusCode Status, SdkHistoryReadResult Result) ReadStatus(OtOpcUaSdkServer server, OtOpcUaNodeManager nm,
NodeId notifier, SimpleAttributeOperandCollection select, ContentFilter? where)
NodeId notifier, List<SimpleAttributeOperand> select, ContentFilter? where)
{
var details = new ReadEventDetails
{
@@ -156,7 +154,7 @@ public sealed class NodeManagerHistoryReadEventsProjectionTests : IDisposable
NumValuesPerNode = 100,
Filter = new EventFilter { SelectClauses = select, WhereClause = where ?? new ContentFilter() },
};
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, requestLifetime: RequestLifetime.None, identity: null);
var results = new List<SdkHistoryReadResult> { null! };
var errors = new List<ServiceResult> { null! };
nm.HistoryRead(context, details, TimestampsToReturn.Both, releaseContinuationPoints: false,