test: preserve server SDK 2.0 value security and history contracts
This commit is contained in:
+34
-34
@@ -32,9 +32,9 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
try
|
||||
{
|
||||
var r = Read(nm, node, Epoch, Epoch.AddHours(1), 10, null);
|
||||
r.Data!.DataValues.Single().Value.ShouldBeOfType<float>().ShouldBe(2f);
|
||||
r.Data.ModificationInfos.Single().UserName.ShouldBe("alice");
|
||||
r.Data.ModificationInfos.Single().UpdateType.ShouldBe(HistoryUpdateType.Replace);
|
||||
SdkValueBoundary.ToClr(r.Data!.DataValues.ToArray()!.Single().WrappedValue)!.ShouldBeOfType<float>().ShouldBe(2f);
|
||||
r.Data.ModificationInfos.ToArray()!.Single().UserName.ShouldBe("alice");
|
||||
r.Data.ModificationInfos.ToArray()!.Single().UpdateType.ShouldBe(HistoryUpdateType.Replace);
|
||||
}
|
||||
finally { await host.DisposeAsync(); }
|
||||
}
|
||||
@@ -47,8 +47,8 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
{
|
||||
src.Throw = new System.IO.IOException("connection lost");
|
||||
var r = Read(nm, node, Epoch, Epoch.AddHours(1), 10, null);
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication);
|
||||
r.Result.StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication);
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication.Code);
|
||||
r.Result.StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication.Code);
|
||||
}
|
||||
finally { await host.DisposeAsync(); }
|
||||
}
|
||||
@@ -67,8 +67,8 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
|
||||
var r = Read(nm, node, Epoch, Epoch.AddHours(1), max: 100, cp: null);
|
||||
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
r.Result.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
r.Result.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
r.Cp.ShouldBeNull();
|
||||
src.LastTag.ShouldBe(Tag);
|
||||
src.LastStart.ShouldBe(Epoch);
|
||||
@@ -79,16 +79,16 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
var data = r.Data!;
|
||||
data.DataValues.Count.ShouldBe(3);
|
||||
data.ModificationInfos.Count.ShouldBe(3);
|
||||
data.ModificationInfos.Select(m => m.UpdateType)
|
||||
data.ModificationInfos.ToArray()!.Select(m => m.UpdateType)
|
||||
.ShouldBe([HistoryUpdateType.Insert, HistoryUpdateType.Replace, HistoryUpdateType.Delete]);
|
||||
data.ModificationInfos.Select(m => m.UserName).ShouldBe(["alice", "bob", "carol"]);
|
||||
data.ModificationInfos.Select(m => m.ModificationTime)
|
||||
data.ModificationInfos.ToArray()!.Select(m => m.UserName).ShouldBe(["alice", "bob", "carol"]);
|
||||
data.ModificationInfos.ToArray()!.Select(m => m.ModificationTime)
|
||||
.ShouldBe([modifiedAt, modifiedAt.AddMinutes(1), modifiedAt.AddMinutes(2)]);
|
||||
data.DataValues[0].Value.ShouldBeNull();
|
||||
data.DataValues[0].StatusCode.Code.ShouldBe(StatusCodes.BadNoData);
|
||||
data.DataValues[1].Value.ShouldBe(1.5);
|
||||
SdkValueBoundary.ToClr(data.DataValues[0].WrappedValue)!.ShouldBeNull();
|
||||
data.DataValues[0].StatusCode.Code.ShouldBe(StatusCodes.BadNoData.Code);
|
||||
SdkValueBoundary.ToClr(data.DataValues[1].WrappedValue)!.ShouldBe(1.5);
|
||||
data.DataValues[1].SourceTimestamp.ShouldBe(Epoch.AddSeconds(10));
|
||||
data.DataValues[2].Value.ShouldBe(7.0);
|
||||
SdkValueBoundary.ToClr(data.DataValues[2].WrappedValue)!.ShouldBe(7.0);
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
{
|
||||
var (host, nm, node, _) = await SetupAsync([]);
|
||||
var r = Read(nm, node, Epoch, Epoch.AddHours(1), max: 10, cp: null);
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
r.Result.StatusCode.Code.ShouldBe(StatusCodes.GoodNoData);
|
||||
r.Data!.ModificationInfos.ShouldBeEmpty();
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
r.Result.StatusCode.Code.ShouldBe(StatusCodes.GoodNoData.Code);
|
||||
r.Data!.ModificationInfos.Count.ShouldBe(0);
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
{
|
||||
// Inserts have no prior value; their snapshot has no source timestamp. The cursor must not fall back to MinValue.
|
||||
var trail = Enumerable.Range(1, 6).Select(i => new HistoryModifiedValue(
|
||||
new DataValueSnapshot(null, StatusCodes.BadNoData, null, Epoch), Epoch, HistoryModificationType.Insert, "u", i,
|
||||
new DataValueSnapshot(null, StatusCodes.BadNoData.Code, null, Epoch), Epoch, HistoryModificationType.Insert, "u", i,
|
||||
Epoch.AddSeconds(i))).ToList();
|
||||
var (host, nm, node, src) = await SetupAsync(trail);
|
||||
|
||||
@@ -214,20 +214,20 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
// A RAW cursor for the same tag is not a modified cursor.
|
||||
var raw = nm.HistoryContinuationStore.Save(null, new HistoryContinuationState(Tag, Epoch, Epoch.AddDays(1), 0, 4));
|
||||
Read(nm, node, Epoch, Epoch.AddHours(1), max: 4, cp: raw).Error.StatusCode.Code
|
||||
.ShouldBe(StatusCodes.BadContinuationPointInvalid);
|
||||
.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
|
||||
|
||||
// A MODIFIED cursor presented to a raw read is invalid too.
|
||||
var p1 = Read(nm, node, Epoch, Epoch.AddHours(1), max: 4, cp: null);
|
||||
p1.Cp.ShouldNotBeNull();
|
||||
Read(nm, node, Epoch, Epoch.AddHours(1), max: 4, cp: p1.Cp, modified: false).Error.StatusCode.Code
|
||||
.ShouldBe(StatusCodes.BadContinuationPointInvalid);
|
||||
.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
|
||||
|
||||
// A modified cursor issued for one node is invalid on another node's tag.
|
||||
var p2 = Read(nm, node, Epoch, Epoch.AddHours(1), max: 4, cp: null);
|
||||
nm.EnsureVariable("eq-1/other", null, "Other", "Double", false, historianTagname: "WW.Other", realm: AddressSpaceRealm.Uns);
|
||||
var other = nm.TryGetVariable("eq-1/other")!.NodeId;
|
||||
Read(nm, other, Epoch, Epoch.AddHours(1), max: 4, cp: p2.Cp).Error.StatusCode.Code
|
||||
.ShouldBe(StatusCodes.BadContinuationPointInvalid);
|
||||
.ShouldBe(StatusCodes.BadContinuationPointInvalid.Code);
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -245,9 +245,9 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
var modified = Read(nm, node, Epoch, Epoch.AddHours(1), max: 10, cp: null);
|
||||
var raw = Read(nm, node, Epoch, Epoch.AddHours(1), max: 10, cp: null, modified: false);
|
||||
|
||||
modified.Error.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
modified.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code);
|
||||
modified.Data!.DataValues.Count.ShouldBe(1);
|
||||
raw.Error.StatusCode.Code.ShouldNotBe(StatusCodes.BadInvalidState);
|
||||
raw.Error.StatusCode.Code.ShouldNotBe(StatusCodes.BadInvalidState.Code);
|
||||
src.Calls.ShouldBe(1);
|
||||
consulted.ShouldBe(0);
|
||||
await host.DisposeAsync();
|
||||
@@ -259,7 +259,7 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
var (host, nm, node, src) = await SetupAsync([Mod(1, 1, 1, HistoryModificationType.Update, "u", Epoch)]);
|
||||
src.Throw = new InvalidOperationException("boom");
|
||||
var r = Read(nm, node, Epoch, Epoch.AddHours(1), max: 4, cp: null);
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported);
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadHistoryOperationUnsupported.Code);
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -270,8 +270,8 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
{
|
||||
var t = Epoch.AddSeconds(seconds);
|
||||
var value = prior is null
|
||||
? new DataValueSnapshot(null, StatusCodes.BadNoData, t, modifiedAt)
|
||||
: new DataValueSnapshot(prior.Value, StatusCodes.Good, t, t);
|
||||
? new DataValueSnapshot(null, StatusCodes.BadNoData.Code, t, modifiedAt)
|
||||
: new DataValueSnapshot(prior.Value, StatusCodes.Good.Code, t, t);
|
||||
return new HistoryModifiedValue(value, modifiedAt, type, user, id, t);
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
do
|
||||
{
|
||||
var r = Read(nm, node, start, end, max, cp);
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good, r.Error.ToString());
|
||||
r.Error.StatusCode.Code.ShouldBe(StatusCodes.Good.Code, r.Error.ToString());
|
||||
ids.AddRange(r.Ids);
|
||||
cp = r.Cp;
|
||||
(++pages).ShouldBeLessThan(60, "paging must terminate");
|
||||
@@ -295,25 +295,25 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
private static (ServiceResult Error, SdkHistoryReadResult Result, HistoryModifiedData? Data, byte[]? Cp, List<long> Ids) Read(
|
||||
OtOpcUaNodeManager nm, NodeId node, DateTime start, DateTime end, uint max, byte[]? cp, bool modified = true)
|
||||
{
|
||||
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,
|
||||
new ReadRawModifiedDetails { ReturnBounds = false, StartTime = start, EndTime = end, NumValuesPerNode = max, IsReadModified = modified },
|
||||
TimestampsToReturn.Both, false,
|
||||
new List<HistoryReadValueId> { new() { NodeId = node, ContinuationPoint = cp } }, results, errors);
|
||||
new List<HistoryReadValueId> { new() { NodeId = node, ContinuationPoint = ByteString.From(cp) } }, results, errors);
|
||||
|
||||
HistoryModifiedData? data = null;
|
||||
var ids = new List<long>();
|
||||
if (results[0]?.HistoryData is { } ho && !ExtensionObject.IsNull(ho)
|
||||
&& ExtensionObject.ToEncodeable(ho) is HistoryModifiedData hm)
|
||||
if (results[0]?.HistoryData is { } ho && !ho.IsNull
|
||||
&& ExtensionObject.ToEncodeable(ho)! is HistoryModifiedData hm)
|
||||
{
|
||||
data = hm;
|
||||
// The fake stores each modification's id in its (unique) server timestamp offset — see SqlLikeTrail.
|
||||
ids.AddRange(hm.DataValues.Select(v => SqlLikeTrail.IdOf(v)));
|
||||
ids.AddRange(hm.DataValues.ToArray()!.Select(v => SqlLikeTrail.IdOf(v)));
|
||||
}
|
||||
|
||||
return (errors[0], results[0], data, results[0]?.ContinuationPoint, ids);
|
||||
return (errors[0], results[0], data, results[0]?.ContinuationPoint.ToNullableArray(), ids);
|
||||
}
|
||||
|
||||
private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager Nm, NodeId Node, SqlLikeTrail Src)> SetupAsync(
|
||||
@@ -355,7 +355,7 @@ public sealed class NodeManagerHistoryReadModifiedTests : IDisposable
|
||||
public int Calls { get; private set; }
|
||||
public Exception? Throw { get; set; }
|
||||
|
||||
public static long IdOf(DataValue v) => v.ServerTimestamp.Ticks - Epoch.Ticks;
|
||||
public static long IdOf(DataValue v) => v.ServerTimestamp.ToDateTime().Ticks - Epoch.Ticks;
|
||||
|
||||
public IEnumerable<HistoryModifiedValue> Ordered(bool reverse) => reverse
|
||||
? trail.OrderByDescending(m => m.SourceTimeUtc).ThenByDescending(m => m.ModifiedId)
|
||||
|
||||
Reference in New Issue
Block a user