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
@@ -69,7 +69,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
var p1 = ReadEvents(nm, node, max: 3, inboundCp: null);
p1.Cp.ShouldNotBeNull();
var p2 = ReadEvents(nm, node, max: 3, inboundCp: p1.Cp);
p2.Error.StatusCode.Code.ShouldBe(StatusCodes.BadTooManyOperations);
p2.Error.StatusCode.Code.ShouldBe(StatusCodes.BadTooManyOperations.Code);
await host.DisposeAsync();
}
@@ -78,7 +78,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
{
var (host, nm, node, _) = await SetupAsync(Distinct(5));
var r = ReadEvents(nm, node, max: 2, inboundCp: Guid.NewGuid().ToByteArray());
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadContinuationPointInvalid);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
await host.DisposeAsync();
}
@@ -90,7 +90,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
p1.Cp.ShouldNotBeNull();
InvokeRelease(nm, node, p1.Cp!);
ReadEvents(nm, node, max: 4, inboundCp: p1.Cp).Error.StatusCode.Code
.ShouldBe(StatusCodes.BadContinuationPointInvalid);
.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
await host.DisposeAsync();
}
@@ -120,7 +120,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
var (host, nm, node, _) = await SetupAsync(Distinct(10));
var raw = nm.HistoryContinuationStore.Save(null, new HistoryContinuationState("t", Epoch, Epoch.AddDays(1), 0, 4));
ReadEvents(nm, node, max: 4, inboundCp: raw).Error.StatusCode.Code
.ShouldBe(StatusCodes.BadContinuationPointInvalid);
.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
await host.DisposeAsync();
}
@@ -141,7 +141,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
var p1 = ReadEvents(nm, node, max: 4, inboundCp: null);
p1.Cp.ShouldNotBeNull();
ReadEvents(nm, other, max: 4, inboundCp: p1.Cp).Error.StatusCode.Code
.ShouldBe(StatusCodes.BadContinuationPointInvalid);
.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
await host.DisposeAsync();
}
@@ -157,21 +157,21 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
byte[] RawRead(NodeId n, byte[]? cp, out ServiceResult err)
{
var ctx = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
var ctx = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, requestLifetime: RequestLifetime.None, identity: null);
var res = new List<SdkHistoryReadResult> { null! };
var errs = new List<ServiceResult> { null! };
nm.HistoryRead(ctx, new ReadRawModifiedDetails { ReturnBounds = false, StartTime = Epoch, EndTime = Epoch.AddHours(1), NumValuesPerNode = 2, IsReadModified = false },
TimestampsToReturn.Both, false,
new List<HistoryReadValueId> { new() { NodeId = n, ContinuationPoint = cp } }, res, errs);
new List<HistoryReadValueId> { new() { NodeId = n, ContinuationPoint = ByteString.From(cp) } }, res, errs);
err = errs[0];
return res[0]?.ContinuationPoint!;
return res[0]?.ContinuationPoint.ToNullableArray()!;
}
var cp = RawRead(a, null, out var err0);
err0.StatusCode.Code.ShouldBe(StatusCodes.Good, err0.ToString());
err0.StatusCode.Code.ShouldBe(StatusCodes.Good.Code, err0.ToString());
cp.ShouldNotBeNull();
RawRead(b, cp, out var err);
err.StatusCode.Code.ShouldBe(StatusCodes.BadContinuationPointInvalid);
err.StatusCode.Code.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
await host.DisposeAsync();
}
@@ -199,10 +199,10 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
good1[1].Cp.ShouldNotBeNull();
var mixed = ReadBatch(nm, 4, null, (node, good1[0].Cp), (other, Guid.NewGuid().ToByteArray()));
mixed[0].Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
mixed[0].Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
// A resumed page is FULL (#675): it over-fetches by the boundary skip it then trims.
mixed[0].Ids.ShouldBe(src.All.Skip(4).Take(4).Select(e => e.EventId));
mixed[1].Error.StatusCode.Code.ShouldBe(StatusCodes.BadContinuationPointInvalid);
mixed[1].Error.StatusCode.Code.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
await host.DisposeAsync();
}
@@ -225,7 +225,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
// The pre-#675 shape: the cap kept the EARLIEST N, ascending, Good, and no continuation point.
var (host, nm, node, _) = await SetupAsync(Distinct(10));
var p1 = ReadEvents(nm, node, max: 3, inboundCp: null, end: Early, start: Late);
p1.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
p1.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
p1.Ids.ShouldBe(["e0009", "e0008", "e0007"]);
p1.Cp.ShouldNotBeNull("a capped reverse read has more behind it and must page");
await host.DisposeAsync();
@@ -283,7 +283,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
var p1 = ReadEvents(nm, node, max: 3, inboundCp: null, end: Early, start: Late);
p1.Cp.ShouldNotBeNull();
ReadEvents(nm, node, max: 3, inboundCp: p1.Cp, end: Early, start: Late).Error.StatusCode.Code
.ShouldBe(StatusCodes.BadTooManyOperations);
.ShouldBe(StatusCodes.BadTooManyOperations.Code);
await host.DisposeAsync();
}
@@ -295,7 +295,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
var p1 = ReadEvents(nm, node, max: 4, inboundCp: null, end: Early, start: Late);
p1.Ids.ShouldBe(["e0009", "e0008", "e0007", "e0006"]);
var p2 = ReadEvents(nm, node, max: 4, inboundCp: p1.Cp); // default details: Early -> Late (forward)
p2.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
p2.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
p2.Ids.ShouldBe(["e0005", "e0004", "e0003", "e0002"]);
await host.DisposeAsync();
}
@@ -340,7 +340,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
var fwd = new ForwardOnlyEventSource(src);
nm.HistorianDataSource = fwd;
var r = ReadEvents(nm, node, max: 3, inboundCp: null, end: Early, start: Late);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
r.Ids.ShouldBe(["e0009", "e0008", "e0007"]);
r.Cp.ShouldNotBeNull();
// Forward reads on the same backend are unaffected.
@@ -469,7 +469,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
break;
}
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good, $"seed {seed}, ms max {msMax}");
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code, $"seed {seed}, ms max {msMax}");
all.AddRange(r.Ids);
cp = r.Cp;
(++pages).ShouldBeLessThan(2000);
@@ -492,7 +492,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
var (host, nm, node, _) = await SetupAsync(Distinct(10), logger: logger);
nm.HistorianDataSource = new ThrowingReverseSource();
ReadEvents(nm, node, max: 3, inboundCp: null, end: Early, start: Late).Error.StatusCode.Code
.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
logger.Entries.ShouldContain(e => e.Level == LogLevel.Error && e.Exception is NotSupportedException);
logger.Entries.ShouldNotContain(e => e.Level == LogLevel.Warning);
await host.DisposeAsync();
@@ -580,7 +580,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
{
public Task<HistorianRead> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct)
{
var rows = Enumerable.Range(0, 10).Select(i => new DataValueSnapshot((double)i, StatusCodes.Good,
var rows = Enumerable.Range(0, 10).Select(i => new DataValueSnapshot((double)i, StatusCodes.Good.Code,
Epoch.AddSeconds(i), Epoch.AddSeconds(i))).Where(v => v.SourceTimestampUtc >= s && v.SourceTimestampUtc <= e)
.Take((int)m).ToList();
return Task.FromResult(new HistorianRead(rows, null));
@@ -646,7 +646,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
do
{
var r = ReadEvents(nm, node, max, cp, end, start);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
all.AddRange(r.Ids);
pageSizes?.Add(r.Ids.Count);
cp = r.Cp;
@@ -668,12 +668,12 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
OtOpcUaNodeManager nm, uint max, DateTime? end, DateTime? start, params (NodeId Node, byte[]? Cp)[] reads)
{
var context = new OperationContext(
new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, requestLifetime: RequestLifetime.None, identity: null);
var filter = new EventFilter();
filter.SelectClauses.Add(new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("EventId"))
filter.SelectClauses = [new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName("EventId"))
{
AttributeId = Attributes.Value,
});
}];
var details = new ReadEventDetails
{
StartTime = start ?? Epoch.AddDays(-1),
@@ -681,7 +681,7 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
NumValuesPerNode = max,
Filter = filter,
};
var nodesToRead = reads.Select(r => new HistoryReadValueId { NodeId = r.Node, ContinuationPoint = r.Cp }).ToList();
var nodesToRead = reads.Select(r => new HistoryReadValueId { NodeId = r.Node, ContinuationPoint = ByteString.From(r.Cp) }).ToList();
var results = reads.Select(_ => (SdkHistoryReadResult)null!).ToList();
var errors = reads.Select(_ => (ServiceResult)null!).ToList();
nm.HistoryRead(context, details, TimestampsToReturn.Both, false, nodesToRead, results, errors);
@@ -690,14 +690,14 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
for (var i = 0; i < reads.Length; i++)
{
var ids = new List<string>();
if (results[i]?.HistoryData is { } ho && !ExtensionObject.IsNull(ho))
if (results[i]?.HistoryData is { } ho && !ho.IsNull)
{
var he = (HistoryEvent)ExtensionObject.ToEncodeable(ho);
var he = (HistoryEvent)ExtensionObject.ToEncodeable(ho)!;
foreach (var e in he.Events)
ids.Add(System.Text.Encoding.UTF8.GetString((byte[])e.EventFields[0].Value));
ids.Add(System.Text.Encoding.UTF8.GetString((byte[])SdkValueBoundary.ToClr(e.EventFields[0])!));
}
all.Add((ids, errors[i], results[i]?.ContinuationPoint));
all.Add((ids, errors[i], results[i]?.ContinuationPoint.ToNullableArray()));
}
return all;
@@ -706,8 +706,8 @@ public sealed class NodeManagerHistoryReadEventsPagingTests : IDisposable
private static void InvokeRelease(OtOpcUaNodeManager nm, NodeId node, byte[] cp)
{
var context = new OperationContext(
new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = node, ContinuationPoint = cp } };
new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, requestLifetime: RequestLifetime.None, identity: null);
var nodesToRead = new List<HistoryReadValueId> { new() { NodeId = node, ContinuationPoint = ByteString.From(cp) } };
var results = new List<SdkHistoryReadResult> { null! };
var errors = new List<ServiceResult> { null! };
nm.HistoryRead(context, new ReadEventDetails { StartTime = Epoch, EndTime = Epoch.AddDays(1), NumValuesPerNode = 4 },