mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
Reduce ISession: type the continuation points, delete the obsolete validate overload (#4199)
## Stacked on #4197 Base is `marcschier/server-bind-phase`. The full stack is **#4188 → #4193 → #4197 → this**. GitHub retargets each base automatically as the stack merges. **Review the earlier three first.** **#4217 has been merged into this branch** (squash `53dd4b618`), so the last remaining item from the issue — `ISession.SessionDiagnostics` — is now part of this PR. Closes #4187. Refs #4203 — **does not close it.** #4217 swapped `OnUpdateDiagnostics` onto `m_diagnosticsLock`, but it still calls `Variant.FromStructure(ServerDiagnostics)` with the default `copy: false`, which wraps the *live* structure in the `ExtensionObject`. The caller reads its fields after the lock is released, so the lock currently excludes nothing. #4222 adds `copy: true` and is what actually closes #4203. ## The finding that changed the approach `ISession` has **exactly one production adapter**: `src/Opc.Ua.Server/Session/Session.cs`. The other `ISession` implementations in the tree are the unrelated **client** interface (`src/Opc.Ua.Client/Session/Session.cs`, `RedundantClientSession`) and a migration-analyzer test stub. One adapter means the seam is hypothetical; two means it is real. The issue proposed splitting `ISession` into five concern-interfaces — that would turn one hypothetical seam into five. The interface earns its keep for testability and because node managers receive it through `OperationContext`, but that argues for making it **smaller**, not for multiplying it. **So: reduce, don't split.** Reasoning posted to the issue as [comment 5226712936](https://github.com/OPCFoundation/UA-.NETStandard/issues/4187#issuecomment-5226712936) so the five-way framing does not get re-proposed later. ## Measurement 38 members, 368 references. External production consumers are **4 files using 5 members**: | Consumer | Members used | |---|---| | `Opc.Ua.Redundancy.Server/Sessions/DistributedSessionManager.cs` | `Id`, `InitializeAsync`, `Dispose` | | `Opc.Ua.Di.Server/Locking/DefaultLockService.cs` | `Id` | | `samples/ConsoleReferenceServer/UAServer.cs` | `Id`, `Identity`, `ReadDiagnostics` | | `samples/Quickstarts.Servers/.../DataChangeMonitoredItem.cs` | type reference only | Every production caller of the five continuation-point members is inside `Opc.Ua.Server` (`MasterNodeManager`, `NodeManagerLifecycle`, `HistorianDispatcher`). **`ISession` is 38 → 32 members.** ## Commits ### `882b2937` Type the history continuation point `ISession` leaked `object` twice, against the repository rule: ```csharp void SaveHistoryContinuationPoint(Guid id, object continuationPoint); object? RestoreHistoryContinuationPoint(ByteString continuationPoint); ``` The erasure was **not** hiding a genuinely opaque plugin token. `HistorianDispatcher` is the only production caller, it always passes a `HistorianContinuationState`, and it casts straight back to that type on restore. The `object` was a round trip through the type system that bought nothing. `HistorianContinuationState` is `internal`, so it cannot appear on a public interface. `IHistoryContinuationPoint` names the concept instead: it carries an `Id` and extends `IDisposable`. Two latent bugs fell out of typing it: - The session disposed only those points that **happened** to implement `IDisposable` and **silently leaked the rest**. Every point is now disposable by construction, and three `as IDisposable` probes are gone. - Carrying the `Id` on the point removes the separate `Guid` parameter, which could disagree with the payload it was filed under. The mock stores in the historian tests had the same latent inconsistency. ### `0d020b77` Delete the obsolete synchronous `ValidateBeforeActivate` **Zero production callers** — the nine references were all tests written for it. `[Obsolete]` since 1.5.378, which the repository rules allow removing in master. It is also the divergent-signature problem the runtime analysis flagged: the async form returns the validated handler and policy as a tuple, while the sync one took the same two values as trailing `out` parameters, so a caller had to learn two shapes for one operation. The overload could not verify a user token requiring decryption, so on a secure endpoint it failed closed and told the caller to use the async path anyway. The one test asserting that fail-closed behaviour goes with the method it described; the path it pointed at is already covered by `ActivationValidationDecodesBinaryEncodedUserIdentityToken`. The other eight tests are **converted, not deleted** — the channel, signature, security-policy and token-policy validation they cover is still asserted, now against the path the server actually uses. ### `821e8302` Route the continuation points behind one member Five `ISession` members were one-line pass-throughs to `SessionContinuationPoints`, a 419-line module that already owns the behaviour and already has its own `IContinuationPointStore` seam. The interface restated the module's surface without adding anything. ```csharp // was session.SaveContinuationPoint(cp); session.RestoreHistoryContinuationPoint(bytes); // now session.ContinuationPoints.SaveBrowse(cp); session.ContinuationPoints.RestoreHistory(bytes); ``` The holder is `internal sealed`, so `ISessionContinuationPoints` is what makes it addressable. **This is composition, not the service locator `IServerContext` refused to be.** A subsystem with an independent lifetime should be injected; continuation points belong to the session and cannot exist without it. `InvalidateContinuationPoints` called `RemoveBrowseForManager` and `RemoveHistoryForManager` in sequence, so the pair becomes `RemoveForManager` — the caller's intent is that a node manager is going away, not that two collections need visiting. ## Tests Three harnesses had hand-rolled a `Dictionary<Guid, object>` to stand in for the holder. They now use the **real** one, so the dispatcher and `BrowseNext` meet the eviction and disposal rules a live session actually applies. **One hazard worth recording for future interface work.** A Moq member that is never stubbed returns `null`. Where the old code called a method straight on `ISession`, that `null` was harmless. Reaching *through* a property means an unstubbed `ContinuationPoints` null-derefs instead. Three tests failed exactly that way; they are wired to a real holder, not restubbed. | Suite | net10.0 | net48 | |---|---|---| | `Opc.Ua.Server.Tests` | 4032 passed, 1 failed | builds clean | | Touched areas (continuation points, historian, master node manager, session) | 898 passed | — | The single failure is `ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`, a local certificate-store problem verified to reproduce unchanged at HEAD with these changes stashed. Not caused by this PR. `Opc.Ua.Redundancy.Server`, `Opc.Ua.Di.Server`, `Quickstarts.Servers` and `ConsoleReferenceServer` all build clean. ## Docs `docs/MigrationGuide.md` gains a "Removed members on ISession" section covering the `ValidateBeforeActivate` removal with the async replacement, and the `IHistoryContinuationPoint` change with before/after. ## The SessionDiagnostics removal (folded in from #4217) `ISession.SessionDiagnostics` handed out the whole mutable `SessionDiagnosticsDataType` — the structure the session's diagnostics lock protects — so a caller could read a field while the owner was writing it. Every server-side reader wanted exactly one value out of it, so the interface now exposes those two values (`SessionName`, `ClientApplicationUri`) directly and the structure stays behind the lock. That work landed here as #4217 rather than as a separate PR, and it also fixes #4203: `OnUpdateDiagnostics` now takes `m_diagnosticsLock` instead of locking the diagnostics payload it was handed. ## Reviewer questions - `ISessionContinuationPoints` exposes `MaxBrowse` as read-only while the concrete holder keeps a setter (the test framework adjusts it to exercise limits). Should the setter be on the interface, or is test-only mutation better left to the concrete type? - `RemoveForManager` collapses two removal methods. The concrete `RemoveBrowseForManager`/`RemoveHistoryForManager` remain public on the holder — worth making them private now that nothing outside calls them individually? --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e9c13dd0-a6e3-4ad2-8ad6-1c3fe6323041
This commit is contained in:
@@ -196,6 +196,76 @@ for the before/after and
|
||||
[Custom node types and assignment control](NodeManagers.md#custom-node-types-and-assignment-control)
|
||||
for the runtime rules.
|
||||
|
||||
## Removed members on ISession
|
||||
|
||||
`ISession.SessionDiagnostics` is removed. It handed out the whole mutable
|
||||
`SessionDiagnosticsDataType` — the structure the session's diagnostics lock
|
||||
protects — so a caller could read a field while the owner was writing it.
|
||||
|
||||
Every server-side reader wanted one value out of it, and those two values are
|
||||
now on the interface directly:
|
||||
|
||||
```csharp
|
||||
// was
|
||||
string? uri = session.SessionDiagnostics?.ClientDescription?.ApplicationUri;
|
||||
string name = session.SessionDiagnostics?.SessionName ?? string.Empty;
|
||||
|
||||
// now
|
||||
string? uri = session.ClientApplicationUri;
|
||||
string name = session.SessionName;
|
||||
```
|
||||
|
||||
For anything else in the structure, project it inside `ReadDiagnostics`, which
|
||||
holds the lock for the duration of the projection:
|
||||
|
||||
```csharp
|
||||
uint reads = session.ReadDiagnostics(diagnostics => diagnostics.ReadCount.TotalCount);
|
||||
```
|
||||
|
||||
`SessionName` is read from the field it was always a copy of rather than from
|
||||
the diagnostics, because it is assigned once during construction and a value
|
||||
that cannot change should not cost a lock.
|
||||
|
||||
The concrete `Session` still exposes `SessionDiagnostics`; only the interface
|
||||
loses it.
|
||||
|
||||
`ISession.ValidateBeforeActivate` — the synchronous overload with
|
||||
`out IUserIdentityTokenHandler?` and `out UserTokenPolicy?` parameters — is
|
||||
removed. It had no caller anywhere in the stack, its samples or its tests other
|
||||
than tests written for it, and it had been `[Obsolete]` since 1.5.378.
|
||||
|
||||
Use `ValidateBeforeActivateAsync`, which returns the same two values as a tuple:
|
||||
|
||||
```csharp
|
||||
(IUserIdentityTokenHandler identityToken, UserTokenPolicy? userTokenPolicy) =
|
||||
await session.ValidateBeforeActivateAsync(
|
||||
context, clientSignature, userIdentityToken, userTokenSignature, ct)
|
||||
.ConfigureAwait(false);
|
||||
```
|
||||
|
||||
The synchronous overload could not verify a user token that required
|
||||
decryption, so on a secure endpoint it failed closed and directed callers to
|
||||
the asynchronous path anyway.
|
||||
|
||||
The history continuation points moved off `ISession` onto
|
||||
`ISession.ContinuationPoints`, and no longer pass `object`. `SaveHistory` and
|
||||
`RestoreHistory` use `IHistoryContinuationPoint`, which carries the point's own
|
||||
`Guid Id` and extends `IDisposable`:
|
||||
|
||||
```csharp
|
||||
// was
|
||||
session.SaveHistoryContinuationPoint(state.Id, state);
|
||||
object? restored = session.RestoreHistoryContinuationPoint(bytes);
|
||||
|
||||
// now
|
||||
session.ContinuationPoints.SaveHistory(state); // the point carries its Id
|
||||
IHistoryContinuationPoint? restored = session.ContinuationPoints.RestoreHistory(bytes);
|
||||
```
|
||||
|
||||
Implement `IHistoryContinuationPoint` on whatever type you store. The session
|
||||
previously disposed only those points that happened to implement `IDisposable`
|
||||
and silently leaked the rest; every point is now disposed.
|
||||
|
||||
## Migrating code that called IServerInternal.Set* mutators
|
||||
|
||||
`IServerInternal` no longer exposes the twelve `Set*` binding methods or
|
||||
|
||||
@@ -3084,7 +3084,7 @@ namespace Opc.Ua.RobotIntent.Server
|
||||
} &&
|
||||
operation.Session != null)
|
||||
{
|
||||
return operation.Session.SessionDiagnostics?.SessionName ?? string.Empty;
|
||||
return operation.Session.SessionName;
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Opc.Ua.Server.Historian
|
||||
/// <para>
|
||||
/// The dispatcher serialises one instance per outstanding paginated
|
||||
/// read into the session's continuation-point dictionary
|
||||
/// (<see cref="Session.SaveHistoryContinuationPoint"/>).
|
||||
/// (<see cref="ISessionContinuationPoints.SaveHistory"/>).
|
||||
/// On the next page request the dispatcher restores the state
|
||||
/// (which removes it from the session's storage), calls the same
|
||||
/// provider with the saved <see cref="ResumeToken"/>, and either
|
||||
@@ -59,7 +59,7 @@ namespace Opc.Ua.Server.Historian
|
||||
/// disposal — the framework guarantees the call.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class HistorianContinuationState : IDisposable
|
||||
internal sealed class HistorianContinuationState : IHistoryContinuationPoint
|
||||
{
|
||||
public required Guid Id { get; set; }
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ namespace Opc.Ua.Server.Historian
|
||||
/// <para>
|
||||
/// The dispatcher is stateless apart from continuation-point storage,
|
||||
/// which lives in the session via
|
||||
/// <see cref="Session.SaveHistoryContinuationPoint"/> /
|
||||
/// <see cref="Session.RestoreHistoryContinuationPoint"/>.
|
||||
/// <see cref="ISessionContinuationPoints.SaveHistory"/> /
|
||||
/// <see cref="ISessionContinuationPoints.RestoreHistory"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class HistorianDispatcher
|
||||
@@ -359,7 +359,7 @@ namespace Opc.Ua.Server.Historian
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="systemContext"/> is <c>null</c>.</exception>
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via SaveHistoryContinuationPoint or disposed inline by EmitProcessedPage.")]
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via ContinuationPoints.SaveHistory or disposed inline by EmitProcessedPage.")]
|
||||
public static async ValueTask<ServiceResult> DispatchProcessedReadAsync(
|
||||
ServerSystemContext systemContext,
|
||||
IHistorianProvider provider,
|
||||
@@ -615,7 +615,7 @@ namespace Opc.Ua.Server.Historian
|
||||
}
|
||||
|
||||
state.Id = Guid.NewGuid();
|
||||
systemContext.OperationContext?.Session?.SaveHistoryContinuationPoint(state.Id, state);
|
||||
systemContext.OperationContext?.Session?.ContinuationPoints.SaveHistory(state);
|
||||
// Per OPC UA Part 11 6.5.3.2 a HistoryRead that returns a ContinuationPoint
|
||||
// (more data available) uses StatusCode Good, not Good_MoreData; the non-empty
|
||||
// ContinuationPoint alone signals to the client that more data can be fetched.
|
||||
@@ -631,7 +631,7 @@ namespace Opc.Ua.Server.Historian
|
||||
/// unsupported for the node.
|
||||
/// </summary>
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via SaveHistoryContinuationPoint or disposed inline by EmitProcessedPage.")]
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via ContinuationPoints.SaveHistory or disposed inline by EmitProcessedPage.")]
|
||||
private static async ValueTask<ServiceResult> ComputeAnnotationCountAsync(
|
||||
ServerSystemContext systemContext,
|
||||
IHistorianProvider provider,
|
||||
@@ -1109,7 +1109,7 @@ namespace Opc.Ua.Server.Historian
|
||||
}
|
||||
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via SaveHistoryContinuationPoint.")]
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via ContinuationPoints.SaveHistory.")]
|
||||
private static void SaveOrReleaseAnnotationContinuation(
|
||||
ServerSystemContext systemContext,
|
||||
HistoryReadValueId nodeToRead,
|
||||
@@ -1154,7 +1154,7 @@ namespace Opc.Ua.Server.Historian
|
||||
}
|
||||
|
||||
state.Id = Guid.NewGuid();
|
||||
systemContext.OperationContext?.Session?.SaveHistoryContinuationPoint(state.Id, state);
|
||||
systemContext.OperationContext?.Session?.ContinuationPoints.SaveHistory(state);
|
||||
// Per OPC UA Part 11 6.5.3.2 a HistoryRead that returns a ContinuationPoint
|
||||
// (more data available) uses StatusCode Good, not Good_MoreData; the non-empty
|
||||
// ContinuationPoint alone signals to the client that more data can be fetched.
|
||||
@@ -1522,7 +1522,7 @@ namespace Opc.Ua.Server.Historian
|
||||
}
|
||||
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via SaveHistoryContinuationPoint.")]
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via ContinuationPoints.SaveHistory.")]
|
||||
private static void SaveOrReleaseEventContinuation(
|
||||
ServerSystemContext systemContext,
|
||||
HistoryReadValueId nodeToRead,
|
||||
@@ -1562,7 +1562,7 @@ namespace Opc.Ua.Server.Historian
|
||||
}
|
||||
|
||||
state.Id = Guid.NewGuid();
|
||||
systemContext.OperationContext?.Session?.SaveHistoryContinuationPoint(state.Id, state);
|
||||
systemContext.OperationContext?.Session?.ContinuationPoints.SaveHistory(state);
|
||||
// Per OPC UA Part 11 6.5.3.2 a HistoryRead that returns a ContinuationPoint
|
||||
// (more data available) uses StatusCode Good, not Good_MoreData; the non-empty
|
||||
// ContinuationPoint alone signals to the client that more data can be fetched.
|
||||
@@ -1594,7 +1594,7 @@ namespace Opc.Ua.Server.Historian
|
||||
return StatusCodes.BadContinuationPointInvalid;
|
||||
}
|
||||
|
||||
object? state = systemContext.OperationContext?.Session?.RestoreHistoryContinuationPoint(
|
||||
IHistoryContinuationPoint? state = systemContext.OperationContext?.Session?.ContinuationPoints.RestoreHistory(
|
||||
nodeToRead.ContinuationPoint);
|
||||
if (state is HistorianContinuationState cont)
|
||||
{
|
||||
@@ -1802,7 +1802,7 @@ namespace Opc.Ua.Server.Historian
|
||||
{
|
||||
return null;
|
||||
}
|
||||
object? raw = systemContext.OperationContext?.Session?.RestoreHistoryContinuationPoint(
|
||||
IHistoryContinuationPoint? raw = systemContext.OperationContext?.Session?.ContinuationPoints.RestoreHistory(
|
||||
nodeToRead.ContinuationPoint);
|
||||
if (raw is not HistorianContinuationState state)
|
||||
{
|
||||
@@ -1832,7 +1832,7 @@ namespace Opc.Ua.Server.Historian
|
||||
}
|
||||
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via SaveHistoryContinuationPoint.")]
|
||||
Justification = "HistorianContinuationState ownership is transferred to the session via ContinuationPoints.SaveHistory.")]
|
||||
private static void SaveOrReleaseContinuation(
|
||||
ServerSystemContext systemContext,
|
||||
HistoryReadValueId nodeToRead,
|
||||
@@ -1885,7 +1885,7 @@ namespace Opc.Ua.Server.Historian
|
||||
}
|
||||
|
||||
state.Id = Guid.NewGuid();
|
||||
systemContext.OperationContext?.Session?.SaveHistoryContinuationPoint(state.Id, state);
|
||||
systemContext.OperationContext?.Session?.ContinuationPoints.SaveHistory(state);
|
||||
// Per OPC UA Part 11 6.5.3.2 a HistoryRead that returns a ContinuationPoint
|
||||
// (more data available) uses StatusCode Good, not Good_MoreData; the non-empty
|
||||
// ContinuationPoint alone signals to the client that more data can be fetched.
|
||||
|
||||
@@ -2167,7 +2167,7 @@ namespace Opc.Ua.Server
|
||||
{
|
||||
foreach (ISession session in server.SessionManager.GetSessions())
|
||||
{
|
||||
session.InvalidateContinuationPoints(nodeManager);
|
||||
session.ContinuationPoints.RemoveForManager(nodeManager);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3424,7 +3424,7 @@ namespace Opc.Ua.Server
|
||||
if (current != null && !current.ContinuationPoint.IsEmpty)
|
||||
{
|
||||
ContinuationPoint? cp = context.Session
|
||||
.RestoreContinuationPoint(current.ContinuationPoint);
|
||||
.ContinuationPoints.RestoreBrowse(current.ContinuationPoint);
|
||||
cp?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -3582,7 +3582,7 @@ namespace Opc.Ua.Server
|
||||
if (current != null && !current.ContinuationPoint.IsEmpty)
|
||||
{
|
||||
cp = context.Session
|
||||
.RestoreContinuationPoint(current.ContinuationPoint);
|
||||
.ContinuationPoints.RestoreBrowse(current.ContinuationPoint);
|
||||
cp?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -3591,7 +3591,7 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
|
||||
// find the continuation point.
|
||||
cp = context.Session.RestoreContinuationPoint(continuationPoints[ii]);
|
||||
cp = context.Session.ContinuationPoints.RestoreBrowse(continuationPoints[ii]);
|
||||
|
||||
// validate access rights and role permissions
|
||||
if (cp != null)
|
||||
@@ -3898,7 +3898,7 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
|
||||
currentCp.Id = Guid.NewGuid();
|
||||
context!.Session!.SaveContinuationPoint(currentCp);
|
||||
context!.Session!.ContinuationPoints.SaveBrowse(currentCp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1424,12 +1424,21 @@ namespace Opc.Ua.Server
|
||||
/// <summary>
|
||||
/// Returns a copy of the current diagnostics.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Returns a copy of the current diagnostics.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Takes the same lock every writer takes. Locking the diagnostics object instead
|
||||
/// would be a different monitor, so the snapshot would not be excluded from a
|
||||
/// concurrent <see cref="UpdateServerDiagnostics"/> and could walk the structure
|
||||
/// while its fields were being written.
|
||||
/// </remarks>
|
||||
private ServiceResult OnUpdateDiagnostics(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
ref Variant value)
|
||||
{
|
||||
lock (ServerDiagnostics)
|
||||
lock (m_diagnosticsLock)
|
||||
{
|
||||
value = Variant.FromStructure(ServerDiagnostics);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
||||
*
|
||||
* OPC Foundation MIT License 1.00
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* The complete license agreement can be found here:
|
||||
* http://opcfoundation.org/License/MIT/1.00/
|
||||
* ======================================================================*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Opc.Ua.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// State a historical read leaves behind so a later HistoryRead can resume where the
|
||||
/// previous one stopped. The session holds it between calls and disposes it when the
|
||||
/// point is dropped or the session is cleared.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations carry whatever the reader needs to resume; nothing outside the
|
||||
/// component that created a point interprets its contents.
|
||||
/// </remarks>
|
||||
public interface IHistoryContinuationPoint : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the continuation point. It is the identifier the client receives and
|
||||
/// returns, so it must not change once the point has been saved.
|
||||
/// </summary>
|
||||
Guid Id { get; }
|
||||
}
|
||||
}
|
||||
@@ -52,12 +52,10 @@ namespace Opc.Ua.Server
|
||||
bool IsClosing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Removes and disposes the saved continuation points that retain the NodeManager, so it
|
||||
/// can be torn down without a Client being able to resume a Browse or a history read into
|
||||
/// it.
|
||||
/// The continuation points this session is holding for its client, for browses and
|
||||
/// for historical reads.
|
||||
/// </summary>
|
||||
/// <param name="nodeManager">The NodeManager being retired.</param>
|
||||
void InvalidateContinuationPoints(IAsyncNodeManager nodeManager);
|
||||
ISessionContinuationPoints ContinuationPoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The server application instance certificate used by this session.
|
||||
@@ -183,9 +181,15 @@ namespace Opc.Ua.Server
|
||||
string SecureChannelId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The diagnostics associated with the session.
|
||||
/// The name the client gave this session when it created it.
|
||||
/// </summary>
|
||||
SessionDiagnosticsDataType SessionDiagnostics { get; }
|
||||
string SessionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The application URI of the client that owns this session, or <c>null</c> when the
|
||||
/// client did not supply an application description.
|
||||
/// </summary>
|
||||
string? ClientApplicationUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Completes the asynchronous part of session creation by registering
|
||||
@@ -226,40 +230,6 @@ namespace Opc.Ua.Server
|
||||
/// </summary>
|
||||
bool IsSecureChannelValid(string secureChannelId);
|
||||
|
||||
/// <summary>
|
||||
/// Restores a continuation point for a session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The caller is responsible for disposing the continuation point returned.
|
||||
/// </remarks>
|
||||
ContinuationPoint? RestoreContinuationPoint(ByteString continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Restores a previously saves history continuation point.
|
||||
/// </summary>
|
||||
/// <param name="continuationPoint">The identifier for the continuation point.</param>
|
||||
/// <returns>The save continuation point. null if not found.</returns>
|
||||
object? RestoreHistoryContinuationPoint(ByteString continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a continuation point for a session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the session has too many continuation points the oldest one is dropped.
|
||||
/// </remarks>
|
||||
void SaveContinuationPoint(ContinuationPoint continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a continuation point used for historical reads.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the continuation point.</param>
|
||||
/// <param name="continuationPoint">The continuation point.</param>
|
||||
/// <remarks>
|
||||
/// If the continuationPoint implements IDisposable it will be disposed when
|
||||
/// the Session is closed or discarded.
|
||||
/// </remarks>
|
||||
void SaveHistoryContinuationPoint(Guid id, object continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Set the ECC security policy URI
|
||||
/// </summary>
|
||||
@@ -289,22 +259,6 @@ namespace Opc.Ua.Server
|
||||
SignatureData userTokenSignature,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the application signature and user identity token before activation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retained for compatibility with 1.5.378 implementations. New code should use
|
||||
/// <see cref="ValidateBeforeActivateAsync"/>.
|
||||
/// </remarks>
|
||||
[Obsolete("Use ValidateBeforeActivateAsync instead.")]
|
||||
void ValidateBeforeActivate(
|
||||
OperationContext context,
|
||||
SignatureData clientSignature,
|
||||
ExtensionObject userIdentityToken,
|
||||
SignatureData userTokenSignature,
|
||||
out IUserIdentityTokenHandler? identityToken,
|
||||
out UserTokenPolicy? userTokenPolicy);
|
||||
|
||||
/// <summary>
|
||||
/// Validate the diagnostic info.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
||||
*
|
||||
* OPC Foundation MIT License 1.00
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* The complete license agreement can be found here:
|
||||
* http://opcfoundation.org/License/MIT/1.00/
|
||||
* ======================================================================*/
|
||||
|
||||
namespace Opc.Ua.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// The continuation points a session is holding on behalf of its client, for browses
|
||||
/// and for historical reads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A continuation point survives between service calls, so the session owns the
|
||||
/// lifetime: points are dropped when the per-session limit is reached, when the node
|
||||
/// manager that issued them goes away, and when the session closes. A dropped history
|
||||
/// point is disposed.
|
||||
/// </remarks>
|
||||
public interface ISessionContinuationPoints
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of browse continuation points the session will hold before it starts
|
||||
/// dropping the oldest.
|
||||
/// </summary>
|
||||
int MaxBrowse { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Saves a browse continuation point, dropping the oldest when the limit is reached.
|
||||
/// </summary>
|
||||
/// <param name="continuationPoint">The continuation point.</param>
|
||||
void SaveBrowse(ContinuationPoint continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Restores and removes a browse continuation point.
|
||||
/// </summary>
|
||||
/// <param name="continuationPoint">The identifier the client returned.</param>
|
||||
/// <returns>The continuation point, or <c>null</c> when it is not held.</returns>
|
||||
ContinuationPoint? RestoreBrowse(ByteString continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a history continuation point, dropping and disposing the oldest when the
|
||||
/// limit is reached.
|
||||
/// </summary>
|
||||
/// <param name="continuationPoint">The continuation point.</param>
|
||||
void SaveHistory(IHistoryContinuationPoint continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Restores and removes a history continuation point.
|
||||
/// </summary>
|
||||
/// <param name="continuationPoint">The identifier the client returned.</param>
|
||||
/// <returns>The continuation point, or <c>null</c> when it is not held.</returns>
|
||||
IHistoryContinuationPoint? RestoreHistory(ByteString continuationPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Drops every point issued by a node manager that is going away, so nothing
|
||||
/// resumes against an address space that no longer exists.
|
||||
/// </summary>
|
||||
/// <param name="nodeManager">The node manager being removed.</param>
|
||||
void RemoveForManager(IAsyncNodeManager nodeManager);
|
||||
}
|
||||
}
|
||||
@@ -379,8 +379,26 @@ namespace Opc.Ua.Server
|
||||
/// <summary>
|
||||
/// The diagnostics associated with the session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not on <see cref="ISession"/>: it is the mutable structure the diagnostics lock
|
||||
/// protects, so handing it out lets a caller read a field the owner may be writing.
|
||||
/// Callers reach values through <see cref="ReadDiagnostics{TResult}"/>, or through
|
||||
/// <see cref="SessionName"/> and <see cref="ClientApplicationUri"/> for the two the
|
||||
/// server itself needs.
|
||||
/// </remarks>
|
||||
public SessionDiagnosticsDataType SessionDiagnostics { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Read from the field rather than from the diagnostics: it is assigned once during
|
||||
/// construction and never changes, so no lock is involved.
|
||||
/// </remarks>
|
||||
public string SessionName => m_sessionName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? ClientApplicationUri
|
||||
=> ReadDiagnostics(diagnostics => diagnostics.ClientDescription?.ApplicationUri);
|
||||
|
||||
/// <summary>
|
||||
/// The client Nonce associated with the session.
|
||||
/// </summary>
|
||||
@@ -619,31 +637,6 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the session and binds it to the current secure channel.
|
||||
/// </summary>
|
||||
/// <exception cref="ServiceResultException"></exception>
|
||||
public void ValidateBeforeActivate(
|
||||
OperationContext context,
|
||||
SignatureData clientSignature,
|
||||
ExtensionObject userIdentityToken,
|
||||
SignatureData userTokenSignature,
|
||||
out IUserIdentityTokenHandler? identityToken,
|
||||
out UserTokenPolicy? userTokenPolicy)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
ValidateChannelBeforeActivate(context, clientSignature);
|
||||
|
||||
// validate the user identity token.
|
||||
identityToken = ValidateUserIdentityToken(
|
||||
userIdentityToken,
|
||||
out userTokenPolicy);
|
||||
|
||||
TraceState("VALIDATED");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<(
|
||||
IUserIdentityTokenHandler IdentityToken,
|
||||
@@ -841,60 +834,8 @@ namespace Opc.Ua.Server
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a continuation point for a session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the session has too many continuation points the oldest one is dropped.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="continuationPoint"/> is <c>null</c>.</exception>
|
||||
public void SaveContinuationPoint(ContinuationPoint continuationPoint)
|
||||
{
|
||||
m_continuationPoints.SaveBrowse(continuationPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a continuation point for a session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The caller is responsible for disposing the continuation point returned.
|
||||
/// </remarks>
|
||||
public ContinuationPoint? RestoreContinuationPoint(ByteString continuationPoint)
|
||||
{
|
||||
return m_continuationPoints.RestoreBrowse(continuationPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void InvalidateContinuationPoints(IAsyncNodeManager nodeManager)
|
||||
{
|
||||
m_continuationPoints.RemoveBrowseForManager(nodeManager);
|
||||
m_continuationPoints.RemoveHistoryForManager(nodeManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a continuation point used for historical reads.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the continuation point.</param>
|
||||
/// <param name="continuationPoint">The continuation point.</param>
|
||||
/// <remarks>
|
||||
/// If the continuationPoint implements IDisposable it will be disposed when
|
||||
/// the Session is closed or discarded.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="continuationPoint"/> is <c>null</c>.</exception>
|
||||
public void SaveHistoryContinuationPoint(Guid id, object continuationPoint)
|
||||
{
|
||||
m_continuationPoints.SaveHistory(id, continuationPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a previously saves history continuation point.
|
||||
/// </summary>
|
||||
/// <param name="continuationPoint">The identifier for the continuation point.</param>
|
||||
/// <returns>The save continuation point. null if not found.</returns>
|
||||
public object? RestoreHistoryContinuationPoint(ByteString continuationPoint)
|
||||
{
|
||||
return m_continuationPoints.RestoreHistory(continuationPoint);
|
||||
}
|
||||
public ISessionContinuationPoints ContinuationPoints => m_continuationPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Loads mirrored continuation point envelopes for a session restored on a backup replica.
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Opc.Ua.Server
|
||||
/// them for cross-replica takeover. Keeping this here lets <see cref="Session"/> delegate through a small surface
|
||||
/// (save/restore/load/clear) instead of managing the store, lists, and dictionaries inline.
|
||||
/// </summary>
|
||||
internal sealed class SessionContinuationPoints
|
||||
internal sealed class SessionContinuationPoints : ISessionContinuationPoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the continuation-point holder for a session.
|
||||
@@ -141,6 +141,13 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RemoveForManager(IAsyncNodeManager nodeManager)
|
||||
{
|
||||
RemoveBrowseForManager(nodeManager);
|
||||
RemoveHistoryForManager(nodeManager);
|
||||
}
|
||||
|
||||
public void RemoveBrowseForManager(IAsyncNodeManager nodeManager)
|
||||
{
|
||||
if (nodeManager is null)
|
||||
@@ -262,17 +269,19 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a history continuation point, dropping the oldest when the limit is reached. A point that implements
|
||||
/// <see cref="IDisposable"/> is disposed when it is dropped or the session is cleared.
|
||||
/// Saves a history continuation point, dropping the oldest when the limit is reached. The
|
||||
/// dropped point is disposed, as is every point still held when the session is cleared.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="continuationPoint"/> is <c>null</c>.</exception>
|
||||
public void SaveHistory(Guid id, object continuationPoint)
|
||||
public void SaveHistory(IHistoryContinuationPoint continuationPoint)
|
||||
{
|
||||
if (continuationPoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(continuationPoint));
|
||||
}
|
||||
|
||||
Guid id = continuationPoint.Id;
|
||||
|
||||
lock (m_lock)
|
||||
{
|
||||
m_history ??= [];
|
||||
@@ -283,7 +292,7 @@ namespace Opc.Ua.Server
|
||||
HistoryContinuationPoint oldCP = m_history[0];
|
||||
m_history.RemoveAt(0);
|
||||
m_store?.RemoveContinuationPoint(Id, ContinuationPointKind.History, oldCP.Id);
|
||||
(oldCP.Value as IDisposable)?.Dispose();
|
||||
oldCP.Value.Dispose();
|
||||
}
|
||||
|
||||
// create the cp.
|
||||
@@ -303,7 +312,7 @@ namespace Opc.Ua.Server
|
||||
/// <summary>
|
||||
/// Restores (and removes) a previously saved history continuation point, or <c>null</c> when not found.
|
||||
/// </summary>
|
||||
public object? RestoreHistory(ByteString continuationPoint)
|
||||
public IHistoryContinuationPoint? RestoreHistory(ByteString continuationPoint)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
@@ -410,7 +419,7 @@ namespace Opc.Ua.Server
|
||||
for (int ii = 0; ii < historyCPs.Count; ii++)
|
||||
{
|
||||
m_store?.RemoveContinuationPoint(Id, ContinuationPointKind.History, historyCPs[ii].Id);
|
||||
(historyCPs[ii].Value as IDisposable)?.Dispose();
|
||||
historyCPs[ii].Value.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,7 +465,7 @@ namespace Opc.Ua.Server
|
||||
private sealed class HistoryContinuationPoint
|
||||
{
|
||||
public Guid Id;
|
||||
public object? Value;
|
||||
public IHistoryContinuationPoint Value = null!;
|
||||
public DateTime Timestamp;
|
||||
}
|
||||
|
||||
|
||||
@@ -1066,7 +1066,7 @@ namespace Opc.Ua.Server
|
||||
{
|
||||
channelCert = Certificate.FromRawData(rawCert.RawData);
|
||||
}
|
||||
channelAppUri = session.SessionDiagnostics?.ClientDescription?.ApplicationUri;
|
||||
channelAppUri = session.ClientApplicationUri;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1816,7 +1816,7 @@ namespace Opc.Ua.Server
|
||||
return session.ClientCertificate.Thumbprint;
|
||||
}
|
||||
|
||||
string? applicationUri = session?.SessionDiagnostics?.ClientDescription?.ApplicationUri;
|
||||
string? applicationUri = session?.ClientApplicationUri;
|
||||
if (!string.IsNullOrEmpty(applicationUri))
|
||||
{
|
||||
return applicationUri!;
|
||||
|
||||
@@ -913,7 +913,7 @@ namespace Opc.Ua.Server
|
||||
!string.IsNullOrEmpty(m_ownerClientApplicationUri) &&
|
||||
string.Equals(
|
||||
m_ownerClientApplicationUri,
|
||||
targetSession.SessionDiagnostics.ClientDescription.ApplicationUri,
|
||||
targetSession.ClientApplicationUri,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
@@ -1370,8 +1370,7 @@ namespace Opc.Ua.Server
|
||||
session.IdentityToken,
|
||||
session.Identity,
|
||||
out m_ownerClientUserId);
|
||||
m_ownerClientApplicationUri =
|
||||
session.SessionDiagnostics.ClientDescription.ApplicationUri;
|
||||
m_ownerClientApplicationUri = session.ClientApplicationUri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -347,7 +347,7 @@ namespace Opc.Ua.Client.TestFramework
|
||||
if (current != null && !current.ContinuationPoint.IsEmpty)
|
||||
{
|
||||
ContinuationPoint cp = context.Session
|
||||
.RestoreContinuationPoint(current.ContinuationPoint);
|
||||
.ContinuationPoints.RestoreBrowse(current.ContinuationPoint);
|
||||
cp.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ namespace Opc.Ua.Server.Tests.Redundancy
|
||||
var session = new Mock<ISession>();
|
||||
ByteString continuationPoint = Guid.NewGuid().ToByteArray().ToByteString();
|
||||
session
|
||||
.Setup(s => s.RestoreContinuationPoint(continuationPoint))
|
||||
.Setup(s => s.ContinuationPoints.RestoreBrowse(continuationPoint))
|
||||
.Returns((ContinuationPoint?)null);
|
||||
var context = new OperationContext(session.Object, DiagnosticsMasks.None);
|
||||
|
||||
|
||||
@@ -718,25 +718,13 @@ namespace Opc.Ua.Server.Tests.Historian
|
||||
m_continuationStore = [];
|
||||
|
||||
var mockSession = new Mock<ISession>();
|
||||
mockSession
|
||||
.Setup(s => s.SaveHistoryContinuationPoint(It.IsAny<Guid>(), It.IsAny<object>()))
|
||||
.Callback<Guid, object>((id, cp) => m_continuationStore[id] = cp);
|
||||
mockSession
|
||||
.Setup(s => s.RestoreHistoryContinuationPoint(It.IsAny<ByteString>()))
|
||||
.Returns<ByteString>(bs =>
|
||||
{
|
||||
if (bs.Length != 16)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var id = new Guid(bs.ToArray());
|
||||
if (m_continuationStore.TryGetValue(id, out object? state))
|
||||
{
|
||||
m_continuationStore.Remove(id);
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// The real holder, not a stand-in dictionary: the dispatcher then meets the
|
||||
// same eviction and disposal rules a live session applies.
|
||||
var continuationPoints = new SessionContinuationPoints(
|
||||
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null);
|
||||
mockSession.Setup(s => s.ContinuationPoints).Returns(continuationPoints);
|
||||
|
||||
|
||||
MockServer = new Mock<IServerInternal>();
|
||||
MockServer.Setup(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
@@ -793,7 +781,7 @@ namespace Opc.Ua.Server.Tests.Historian
|
||||
HistoryUpdateType.Insert);
|
||||
}
|
||||
|
||||
private readonly Dictionary<Guid, object> m_continuationStore;
|
||||
private readonly Dictionary<Guid, IHistoryContinuationPoint> m_continuationStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,28 +490,16 @@ namespace Opc.Ua.Server.Tests.Historian
|
||||
Provider = new InMemoryHistorianProvider();
|
||||
|
||||
var mockTelemetry = new Mock<ITelemetryContext>();
|
||||
var continuationStore = new Dictionary<Guid, object>();
|
||||
var continuationStore = new Dictionary<Guid, IHistoryContinuationPoint>();
|
||||
|
||||
var mockSession = new Mock<ISession>();
|
||||
mockSession
|
||||
.Setup(s => s.SaveHistoryContinuationPoint(It.IsAny<Guid>(), It.IsAny<object>()))
|
||||
.Callback<Guid, object>((id, cp) => continuationStore[id] = cp);
|
||||
mockSession
|
||||
.Setup(s => s.RestoreHistoryContinuationPoint(It.IsAny<ByteString>()))
|
||||
.Returns<ByteString>(bs =>
|
||||
{
|
||||
if (bs.Length != 16)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var id = new Guid(bs.ToArray());
|
||||
if (continuationStore.TryGetValue(id, out object? state))
|
||||
{
|
||||
continuationStore.Remove(id);
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// The real holder, not a stand-in dictionary: the dispatcher then meets the
|
||||
// same eviction and disposal rules a live session applies.
|
||||
var continuationPoints = new SessionContinuationPoints(
|
||||
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null);
|
||||
mockSession.Setup(s => s.ContinuationPoints).Returns(continuationPoints);
|
||||
|
||||
|
||||
var mockServer = new Mock<IServerInternal>();
|
||||
mockServer.Setup(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
|
||||
@@ -733,21 +733,12 @@ namespace Opc.Ua.Server.Tests.Historian
|
||||
var mockTelemetry = new Mock<ITelemetryContext>();
|
||||
var mockSession = new Mock<ISession>();
|
||||
|
||||
var continuationStore = new Dictionary<Guid, object>();
|
||||
mockSession
|
||||
.Setup(s => s.SaveHistoryContinuationPoint(It.IsAny<Guid>(), It.IsAny<object>()))
|
||||
.Callback<Guid, object>((id, cp) => continuationStore[id] = cp);
|
||||
mockSession
|
||||
.Setup(s => s.RestoreHistoryContinuationPoint(It.IsAny<ByteString>()))
|
||||
.Returns<ByteString>(bs =>
|
||||
{
|
||||
if (bs.Length != 16)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var id = new Guid(bs.ToArray());
|
||||
return continuationStore.TryGetValue(id, out object? s) ? s : null;
|
||||
});
|
||||
// The real holder, not a stand-in dictionary: the dispatcher then meets the
|
||||
// same eviction and disposal rules a live session applies.
|
||||
var continuationPoints = new SessionContinuationPoints(
|
||||
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null);
|
||||
mockSession.Setup(s => s.ContinuationPoints).Returns(continuationPoints);
|
||||
|
||||
|
||||
var mockServer = new Mock<IServerInternal>();
|
||||
mockServer.Setup(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
|
||||
@@ -733,25 +733,13 @@ namespace Opc.Ua.Server.Tests.Historian
|
||||
m_continuationStore = [];
|
||||
|
||||
var mockSession = new Mock<ISession>();
|
||||
mockSession
|
||||
.Setup(s => s.SaveHistoryContinuationPoint(It.IsAny<Guid>(), It.IsAny<object>()))
|
||||
.Callback<Guid, object>((id, cp) => m_continuationStore[id] = cp);
|
||||
mockSession
|
||||
.Setup(s => s.RestoreHistoryContinuationPoint(It.IsAny<ByteString>()))
|
||||
.Returns<ByteString>(bs =>
|
||||
{
|
||||
if (bs.Length != 16)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var id = new Guid(bs.ToArray());
|
||||
if (m_continuationStore.TryGetValue(id, out object? state))
|
||||
{
|
||||
m_continuationStore.Remove(id);
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// The real holder, not a stand-in dictionary: the dispatcher then meets the
|
||||
// same eviction and disposal rules a live session applies.
|
||||
var continuationPoints = new SessionContinuationPoints(
|
||||
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null);
|
||||
mockSession.Setup(s => s.ContinuationPoints).Returns(continuationPoints);
|
||||
|
||||
|
||||
var mockServer = new Mock<IServerInternal>();
|
||||
mockServer.Setup(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
@@ -801,7 +789,7 @@ namespace Opc.Ua.Server.Tests.Historian
|
||||
HistoryUpdateType.Insert);
|
||||
}
|
||||
|
||||
private readonly Dictionary<Guid, object> m_continuationStore;
|
||||
private readonly Dictionary<Guid, IHistoryContinuationPoint> m_continuationStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,6 +680,9 @@ namespace Opc.Ua.Server.Tests.NodeManager
|
||||
var mockSession = new Mock<ISession>();
|
||||
mockSession.Setup(s => s.EffectiveIdentity).Returns(new Mock<IUserIdentity>().Object);
|
||||
mockSession.Setup(s => s.PreferredLocales).Returns([]);
|
||||
mockSession.Setup(s => s.ContinuationPoints).Returns(
|
||||
new SessionContinuationPoints(
|
||||
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null));
|
||||
|
||||
var namespaceTable = new NamespaceTable();
|
||||
namespaceTable.Append(TestNamespaceUri);
|
||||
|
||||
@@ -1577,35 +1577,24 @@ namespace Opc.Ua.Server.Tests
|
||||
var session = new Mock<ISession>();
|
||||
session.Setup(s => s.EffectiveIdentity).Returns(new Mock<IUserIdentity>().Object);
|
||||
session.Setup(s => s.PreferredLocales).Returns([]);
|
||||
session.Setup(s => s.ContinuationPoints).Returns(
|
||||
new SessionContinuationPoints(
|
||||
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null));
|
||||
return new OperationContext(
|
||||
new RequestHeader(), null!, RequestType.Read, RequestLifetime.None, session.Object);
|
||||
}
|
||||
|
||||
private static OperationContext CreateContextWithContinuationStore()
|
||||
{
|
||||
var continuationPoints = new Dictionary<string, ContinuationPoint>();
|
||||
var session = new Mock<ISession>();
|
||||
session.Setup(s => s.EffectiveIdentity).Returns(new Mock<IUserIdentity>().Object);
|
||||
session.Setup(s => s.PreferredLocales).Returns([]);
|
||||
session
|
||||
.Setup(s => s.SaveContinuationPoint(It.IsAny<ContinuationPoint>()))
|
||||
.Callback<ContinuationPoint>(cp =>
|
||||
{
|
||||
continuationPoints[ToContinuationPointKey(cp.Id.ToByteArray().ToByteString())] = cp;
|
||||
});
|
||||
session
|
||||
.Setup(s => s.RestoreContinuationPoint(It.IsAny<ByteString>()))
|
||||
.Returns<ByteString>(cpBytes =>
|
||||
{
|
||||
string key = ToContinuationPointKey(cpBytes);
|
||||
if (continuationPoints.TryGetValue(key, out ContinuationPoint? cp))
|
||||
{
|
||||
continuationPoints.Remove(key);
|
||||
return cp;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
// The real holder, not a stand-in dictionary, so BrowseNext meets the same
|
||||
// lookup and eviction rules a live session applies.
|
||||
var continuationPoints = new SessionContinuationPoints(
|
||||
() => NodeId.Null, maxBrowse: 10, maxHistory: 10, store: null);
|
||||
session.Setup(s => s.ContinuationPoints).Returns(continuationPoints);
|
||||
|
||||
return new OperationContext(
|
||||
new RequestHeader(), null!, RequestType.Read, RequestLifetime.None, session.Object);
|
||||
|
||||
@@ -265,7 +265,7 @@ namespace Opc.Ua.Server.Tests
|
||||
SessionContinuationPoints holder = NewHolder();
|
||||
|
||||
ArgumentNullException ex = Assert.Throws<ArgumentNullException>(
|
||||
() => holder.SaveHistory(Guid.NewGuid(), null!))!;
|
||||
() => holder.SaveHistory(null!))!;
|
||||
Assert.That(ex.ParamName, Is.EqualTo("continuationPoint"));
|
||||
}
|
||||
|
||||
@@ -274,9 +274,9 @@ namespace Opc.Ua.Server.Tests
|
||||
{
|
||||
SessionContinuationPoints holder = NewHolder();
|
||||
var id = Guid.NewGuid();
|
||||
object value = new();
|
||||
var value = new TrackingDisposable(id);
|
||||
|
||||
holder.SaveHistory(id, value);
|
||||
holder.SaveHistory(value);
|
||||
|
||||
Assert.That(holder.RestoreHistory(ToByteString(id)), Is.SameAs(value));
|
||||
// The point is removed on restore, so a second restore misses.
|
||||
@@ -289,12 +289,12 @@ namespace Opc.Ua.Server.Tests
|
||||
var store = new Mock<IContinuationPointStore>(MockBehavior.Loose);
|
||||
SessionContinuationPoints holder = NewHolder(maxHistory: 1, store: store.Object);
|
||||
|
||||
var evicted = new TrackingDisposable();
|
||||
var id1 = Guid.NewGuid();
|
||||
var id2 = Guid.NewGuid();
|
||||
var evicted = new TrackingDisposable(id1);
|
||||
|
||||
holder.SaveHistory(id1, evicted);
|
||||
holder.SaveHistory(id2, new object());
|
||||
holder.SaveHistory(evicted);
|
||||
holder.SaveHistory(new TrackingDisposable(id2));
|
||||
|
||||
Assert.That(evicted.Disposed, Is.True);
|
||||
Assert.That(holder.RestoreHistory(ToByteString(id1)), Is.Null);
|
||||
@@ -316,7 +316,7 @@ namespace Opc.Ua.Server.Tests
|
||||
SessionContinuationPoints holder = NewHolder(store: store.Object);
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
holder.SaveHistory(id, new object());
|
||||
holder.SaveHistory(new TrackingDisposable(id));
|
||||
|
||||
Assert.That(captured, Is.Not.Null);
|
||||
Assert.That(captured!.Id, Is.EqualTo(id));
|
||||
@@ -338,7 +338,7 @@ namespace Opc.Ua.Server.Tests
|
||||
public void RestoreHistoryReturnsNullForWrongLength()
|
||||
{
|
||||
SessionContinuationPoints holder = NewHolder();
|
||||
holder.SaveHistory(Guid.NewGuid(), new object());
|
||||
holder.SaveHistory(new TrackingDisposable(Guid.NewGuid()));
|
||||
|
||||
Assert.That(holder.RestoreHistory(new ByteString("\t"u8.ToArray())), Is.Null);
|
||||
}
|
||||
@@ -347,7 +347,7 @@ namespace Opc.Ua.Server.Tests
|
||||
public void RestoreHistoryReturnsNullWhenNotFound()
|
||||
{
|
||||
SessionContinuationPoints holder = NewHolder();
|
||||
holder.SaveHistory(Guid.NewGuid(), new object());
|
||||
holder.SaveHistory(new TrackingDisposable(Guid.NewGuid()));
|
||||
|
||||
Assert.That(holder.RestoreHistory(ToByteString(Guid.NewGuid())), Is.Null);
|
||||
}
|
||||
@@ -408,7 +408,7 @@ namespace Opc.Ua.Server.Tests
|
||||
|
||||
// Populate the local lists so restore reaches the mirrored-owner lookup.
|
||||
holder.SaveBrowse(NewBrowsePoint());
|
||||
holder.SaveHistory(Guid.NewGuid(), new object());
|
||||
holder.SaveHistory(new TrackingDisposable(Guid.NewGuid()));
|
||||
|
||||
Assert.That(holder.RestoreBrowse(ToByteString(browseId)), Is.Null);
|
||||
Assert.That(holder.RestoreHistory(ToByteString(historyId)), Is.Null);
|
||||
@@ -426,12 +426,12 @@ namespace Opc.Ua.Server.Tests
|
||||
var store = new Mock<IContinuationPointStore>(MockBehavior.Loose);
|
||||
SessionContinuationPoints holder = NewHolder(store: store.Object);
|
||||
|
||||
var browseData = new TrackingDisposable();
|
||||
var historyValue = new TrackingDisposable();
|
||||
ContinuationPoint cp = NewBrowsePoint(data: browseData);
|
||||
var browseData = new TrackingDisposable(Guid.NewGuid());
|
||||
var historyId = Guid.NewGuid();
|
||||
var historyValue = new TrackingDisposable(historyId);
|
||||
ContinuationPoint cp = NewBrowsePoint(data: browseData);
|
||||
holder.SaveBrowse(cp);
|
||||
holder.SaveHistory(historyId, historyValue);
|
||||
holder.SaveHistory(historyValue);
|
||||
|
||||
holder.Clear();
|
||||
|
||||
@@ -482,7 +482,7 @@ namespace Opc.Ua.Server.Tests
|
||||
|
||||
// A continuation point that does not record a provider cannot be attributed to a
|
||||
// NodeManager, so it is left alone rather than dropped on a guess.
|
||||
holder.SaveHistory(id, new object());
|
||||
holder.SaveHistory(new TrackingDisposable(id));
|
||||
|
||||
holder.RemoveHistoryForManager(NewNodeManager(Mock.Of<INodeManager>()));
|
||||
|
||||
@@ -517,8 +517,15 @@ namespace Opc.Ua.Server.Tests
|
||||
return new ByteString(id.ToByteArray());
|
||||
}
|
||||
|
||||
private sealed class TrackingDisposable : IDisposable
|
||||
private sealed class TrackingDisposable : IHistoryContinuationPoint
|
||||
{
|
||||
public TrackingDisposable(Guid? id = null)
|
||||
{
|
||||
Id = id ?? Guid.NewGuid();
|
||||
}
|
||||
|
||||
public Guid Id { get; }
|
||||
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -205,7 +207,7 @@ namespace Opc.Ua.Server.Tests
|
||||
Assert.That(session.SecureChannelId, Is.EqualTo("channel-1"));
|
||||
Assert.That(session.Activated, Is.False);
|
||||
Assert.That(session.SessionDiagnostics, Is.Not.Null);
|
||||
Assert.That(session.SessionDiagnostics.SessionName, Is.EqualTo("CoverageSession"));
|
||||
Assert.That(session.ReadDiagnostics(d => d.SessionName), Is.EqualTo("CoverageSession"));
|
||||
Assert.That(session.Identity, Is.Not.Null);
|
||||
}
|
||||
|
||||
@@ -328,7 +330,7 @@ namespace Opc.Ua.Server.Tests
|
||||
Assert.That(
|
||||
() => session.ValidateRequest(new RequestHeader(), channelContext, requestType),
|
||||
Throws.TypeOf<ServiceResultException>());
|
||||
Assert.That(session.SessionDiagnostics.TotalRequestCount.TotalCount, Is.GreaterThan(0));
|
||||
Assert.That(session.ReadDiagnostics(d => d.TotalRequestCount.TotalCount), Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -359,8 +361,8 @@ namespace Opc.Ua.Server.Tests
|
||||
var id = Guid.NewGuid();
|
||||
var cp = new ContinuationPoint { Id = id };
|
||||
|
||||
session.SaveContinuationPoint(cp);
|
||||
ContinuationPoint? restored = session.RestoreContinuationPoint(
|
||||
session.ContinuationPoints.SaveBrowse(cp);
|
||||
ContinuationPoint? restored = session.ContinuationPoints.RestoreBrowse(
|
||||
new ByteString(id.ToByteArray()));
|
||||
|
||||
Assert.That(restored, Is.Not.Null);
|
||||
@@ -374,7 +376,7 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
|
||||
Assert.That(
|
||||
() => session.SaveContinuationPoint(null!),
|
||||
() => session.ContinuationPoints.SaveBrowse(null!),
|
||||
Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
@@ -382,10 +384,10 @@ namespace Opc.Ua.Server.Tests
|
||||
public void RestoreContinuationPointReturnsNullForUnknownAndBadLength()
|
||||
{
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
session.SaveContinuationPoint(new ContinuationPoint { Id = Guid.NewGuid() });
|
||||
session.ContinuationPoints.SaveBrowse(new ContinuationPoint { Id = Guid.NewGuid() });
|
||||
|
||||
Assert.That(session.RestoreContinuationPoint(new ByteString(new byte[] { 1, 2, 3 })), Is.Null);
|
||||
Assert.That(session.RestoreContinuationPoint(new ByteString(Guid.NewGuid().ToByteArray())), Is.Null);
|
||||
Assert.That(session.ContinuationPoints.RestoreBrowse(new ByteString(new byte[] { 1, 2, 3 })), Is.Null);
|
||||
Assert.That(session.ContinuationPoints.RestoreBrowse(new ByteString(Guid.NewGuid().ToByteArray())), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -395,27 +397,52 @@ namespace Opc.Ua.Server.Tests
|
||||
|
||||
// Capacity is 10 (maxBrowseContinuationPoints); adding 12 evicts the oldest.
|
||||
var first = new ContinuationPoint { Id = Guid.NewGuid() };
|
||||
session.SaveContinuationPoint(first);
|
||||
session.ContinuationPoints.SaveBrowse(first);
|
||||
for (int i = 0; i < 11; i++)
|
||||
{
|
||||
session.SaveContinuationPoint(new ContinuationPoint { Id = Guid.NewGuid() });
|
||||
session.ContinuationPoints.SaveBrowse(new ContinuationPoint { Id = Guid.NewGuid() });
|
||||
}
|
||||
|
||||
Assert.That(
|
||||
session.RestoreContinuationPoint(new ByteString(first.Id.ToByteArray())),
|
||||
session.ContinuationPoints.RestoreBrowse(new ByteString(first.Id.ToByteArray())),
|
||||
Is.Null,
|
||||
"The oldest continuation point should have been evicted.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientApplicationUriReadsTheDiagnosticsUnderTheLock()
|
||||
{
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
|
||||
// The four server-side callers that used to reach through SessionDiagnostics
|
||||
// want exactly this, and it is read under the diagnostics lock rather than off
|
||||
// a handed-out structure.
|
||||
Assert.That(
|
||||
session.ClientApplicationUri,
|
||||
Is.EqualTo(session.ReadDiagnostics(d => d.ClientDescription?.ApplicationUri)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SessionNameDoesNotDependOnTheDiagnostics()
|
||||
{
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
|
||||
// Assigned once during construction, so it is read from the field: a caller
|
||||
// must not have to take the diagnostics lock for a value that never changes.
|
||||
Assert.That(
|
||||
session.SessionName,
|
||||
Is.EqualTo(session.ReadDiagnostics(d => d.SessionName)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveAndRestoreHistoryContinuationPointRoundTrips()
|
||||
{
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
var id = Guid.NewGuid();
|
||||
object payload = new();
|
||||
var payload = new TestHistoryContinuationPoint(id);
|
||||
|
||||
session.SaveHistoryContinuationPoint(id, payload);
|
||||
object? restored = session.RestoreHistoryContinuationPoint(new ByteString(id.ToByteArray()));
|
||||
session.ContinuationPoints.SaveHistory(payload);
|
||||
IHistoryContinuationPoint? restored = session.ContinuationPoints.RestoreHistory(new ByteString(id.ToByteArray()));
|
||||
|
||||
Assert.That(restored, Is.SameAs(payload));
|
||||
}
|
||||
@@ -426,7 +453,7 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
|
||||
Assert.That(
|
||||
() => session.SaveHistoryContinuationPoint(Guid.NewGuid(), null!),
|
||||
() => session.ContinuationPoints.SaveHistory(null!),
|
||||
Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
@@ -434,9 +461,9 @@ namespace Opc.Ua.Server.Tests
|
||||
public void RestoreHistoryContinuationPointReturnsNullForBadLength()
|
||||
{
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
session.SaveHistoryContinuationPoint(Guid.NewGuid(), new object());
|
||||
session.ContinuationPoints.SaveHistory(new TestHistoryContinuationPoint());
|
||||
|
||||
Assert.That(session.RestoreHistoryContinuationPoint(new ByteString(new byte[] { 1, 2 })), Is.Null);
|
||||
Assert.That(session.ContinuationPoints.RestoreHistory(new ByteString(new byte[] { 1, 2 })), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -466,7 +493,7 @@ namespace Opc.Ua.Server.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateDiagnosticInfoGrantsUserPermissionInfoForSecurityAdmin()
|
||||
public async Task ValidateDiagnosticInfoGrantsUserPermissionInfoForSecurityAdminAsync()
|
||||
{
|
||||
UserTokenPolicy[] tokens =
|
||||
[
|
||||
@@ -476,13 +503,12 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(endpoint, channelId: "channel-1");
|
||||
OperationContext context = CreateContext(endpoint, channelId: "channel-1");
|
||||
|
||||
session.ValidateBeforeActivate(
|
||||
(IUserIdentityTokenHandler handler, UserTokenPolicy? _) = await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
out IUserIdentityTokenHandler? handler,
|
||||
out _);
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var effectiveIdentity = new Mock<IUserIdentity>();
|
||||
effectiveIdentity.Setup(i => i.TokenType).Returns(UserTokenType.Anonymous);
|
||||
@@ -520,14 +546,13 @@ namespace Opc.Ua.Server.Tests
|
||||
endpoint, channelId: "channel-1", clientCertificate: clientCertificate);
|
||||
OperationContext context = CreateContext(endpoint, channelId: "channel-1");
|
||||
|
||||
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
||||
() => session.ValidateBeforeActivate(
|
||||
ServiceResultException? ex = Assert.ThrowsAsync<ServiceResultException>(
|
||||
async () => await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
out _,
|
||||
out _));
|
||||
CancellationToken.None).ConfigureAwait(false));
|
||||
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadApplicationSignatureInvalid));
|
||||
}
|
||||
|
||||
@@ -537,14 +562,14 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(CreateEndpoint());
|
||||
var first = Guid.NewGuid();
|
||||
|
||||
session.SaveHistoryContinuationPoint(first, new object());
|
||||
session.ContinuationPoints.SaveHistory(new TestHistoryContinuationPoint(first));
|
||||
for (int i = 0; i < 11; i++)
|
||||
{
|
||||
session.SaveHistoryContinuationPoint(Guid.NewGuid(), new object());
|
||||
session.ContinuationPoints.SaveHistory(new TestHistoryContinuationPoint());
|
||||
}
|
||||
|
||||
Assert.That(
|
||||
session.RestoreHistoryContinuationPoint(new ByteString(first.ToByteArray())),
|
||||
session.ContinuationPoints.RestoreHistory(new ByteString(first.ToByteArray())),
|
||||
Is.Null,
|
||||
"The oldest history continuation point should have been evicted.");
|
||||
}
|
||||
@@ -556,14 +581,13 @@ namespace Opc.Ua.Server.Tests
|
||||
OperationContext context = CreateContext(
|
||||
CreateEndpoint(SecurityPolicies.Basic256Sha256, MessageSecurityMode.Sign));
|
||||
|
||||
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
||||
() => session.ValidateBeforeActivate(
|
||||
ServiceResultException? ex = Assert.ThrowsAsync<ServiceResultException>(
|
||||
async () => await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
out _,
|
||||
out _));
|
||||
CancellationToken.None).ConfigureAwait(false));
|
||||
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected));
|
||||
}
|
||||
|
||||
@@ -573,19 +597,18 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(CreateEndpoint(), channelId: "channel-1");
|
||||
OperationContext context = CreateContext(CreateEndpoint(), channelId: "other-channel");
|
||||
|
||||
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
||||
() => session.ValidateBeforeActivate(
|
||||
ServiceResultException? ex = Assert.ThrowsAsync<ServiceResultException>(
|
||||
async () => await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
out _,
|
||||
out _));
|
||||
CancellationToken.None).ConfigureAwait(false));
|
||||
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateBeforeActivateWithAnonymousTokenSucceeds()
|
||||
public async Task ValidateBeforeActivateWithAnonymousTokenSucceedsAsync()
|
||||
{
|
||||
var tokens = new UserTokenPolicy[]
|
||||
{
|
||||
@@ -595,13 +618,13 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(endpoint);
|
||||
OperationContext context = CreateContext(endpoint);
|
||||
|
||||
session.ValidateBeforeActivate(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
out IUserIdentityTokenHandler? handler,
|
||||
out UserTokenPolicy? policy);
|
||||
(IUserIdentityTokenHandler handler, UserTokenPolicy? policy) =
|
||||
await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
Assert.That(handler, Is.Not.Null);
|
||||
Assert.That(policy, Is.Not.Null);
|
||||
@@ -619,19 +642,18 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(endpoint);
|
||||
OperationContext context = CreateContext(endpoint);
|
||||
|
||||
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
||||
() => session.ValidateBeforeActivate(
|
||||
ServiceResultException? ex = Assert.ThrowsAsync<ServiceResultException>(
|
||||
async () => await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
out _,
|
||||
out _));
|
||||
CancellationToken.None).ConfigureAwait(false));
|
||||
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenRejected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateBeforeActivateWithUserNameTokenResolvesPolicy()
|
||||
public async Task ValidateBeforeActivateWithUserNameTokenResolvesPolicyAsync()
|
||||
{
|
||||
var tokens = new UserTokenPolicy[]
|
||||
{
|
||||
@@ -652,13 +674,13 @@ namespace Opc.Ua.Server.Tests
|
||||
Password = new ByteString(new byte[] { 1, 2, 3 })
|
||||
};
|
||||
|
||||
session.ValidateBeforeActivate(
|
||||
context,
|
||||
new SignatureData(),
|
||||
new ExtensionObject(userToken),
|
||||
new SignatureData(),
|
||||
out IUserIdentityTokenHandler? handler,
|
||||
out UserTokenPolicy? policy);
|
||||
(IUserIdentityTokenHandler handler, UserTokenPolicy? policy) =
|
||||
await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
new ExtensionObject(userToken),
|
||||
new SignatureData(),
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
Assert.That(handler, Is.Not.Null);
|
||||
Assert.That(policy, Is.Not.Null);
|
||||
@@ -666,7 +688,7 @@ namespace Opc.Ua.Server.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ActivateFirstThenReactivateSwitchesChannel()
|
||||
public async Task ActivateFirstThenReactivateSwitchesChannelAsync()
|
||||
{
|
||||
var tokens = new UserTokenPolicy[]
|
||||
{
|
||||
@@ -676,13 +698,12 @@ namespace Opc.Ua.Server.Tests
|
||||
using ServerSession session = CreateSession(endpoint, channelId: "channel-1");
|
||||
OperationContext context = CreateContext(endpoint, channelId: "channel-1");
|
||||
|
||||
session.ValidateBeforeActivate(
|
||||
(IUserIdentityTokenHandler handler, UserTokenPolicy? _) = await session.ValidateBeforeActivateAsync(
|
||||
context,
|
||||
new SignatureData(),
|
||||
default,
|
||||
new SignatureData(),
|
||||
out IUserIdentityTokenHandler? handler,
|
||||
out _);
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
bool changed = session.Activate(
|
||||
context,
|
||||
|
||||
@@ -177,10 +177,10 @@ namespace Opc.Ua.Server.Tests
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
|
||||
session.SaveContinuationPoint(continuationPoint);
|
||||
session.ContinuationPoints.SaveBrowse(continuationPoint);
|
||||
|
||||
byte[] idBytes = continuationPoint.Id.ToByteArray();
|
||||
ContinuationPoint restored = session.RestoreContinuationPoint(idBytes.ToByteString());
|
||||
ContinuationPoint restored = session.ContinuationPoints.RestoreBrowse(idBytes.ToByteString());
|
||||
|
||||
Assert.That(restored, Is.Not.Null);
|
||||
Assert.That(restored.Id, Is.EqualTo(continuationPoint.Id));
|
||||
@@ -193,7 +193,7 @@ namespace Opc.Ua.Server.Tests
|
||||
await CreateAndActivateAsync("RestoreUnknownContinuation").ConfigureAwait(false);
|
||||
|
||||
byte[] unknownId = Guid.NewGuid().ToByteArray();
|
||||
ContinuationPoint result = session.RestoreContinuationPoint(unknownId.ToByteString());
|
||||
ContinuationPoint result = session.ContinuationPoints.RestoreBrowse(unknownId.ToByteString());
|
||||
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
@@ -206,7 +206,7 @@ namespace Opc.Ua.Server.Tests
|
||||
|
||||
// Continuation point IDs must be 16 bytes (Guid); shorter input returns null
|
||||
byte[] shortId = [1, 2, 3];
|
||||
ContinuationPoint result = session.RestoreContinuationPoint(shortId.ToByteString());
|
||||
ContinuationPoint result = session.ContinuationPoints.RestoreBrowse(shortId.ToByteString());
|
||||
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
@@ -218,15 +218,15 @@ namespace Opc.Ua.Server.Tests
|
||||
await CreateAndActivateAsync("RestoreRemovesContinuation").ConfigureAwait(false);
|
||||
|
||||
using var cp = new ContinuationPoint { Id = Guid.NewGuid() };
|
||||
session.SaveContinuationPoint(cp);
|
||||
session.ContinuationPoints.SaveBrowse(cp);
|
||||
byte[] idBytes = cp.Id.ToByteArray();
|
||||
|
||||
// First restore retrieves it
|
||||
ContinuationPoint first = session.RestoreContinuationPoint(idBytes.ToByteString());
|
||||
ContinuationPoint first = session.ContinuationPoints.RestoreBrowse(idBytes.ToByteString());
|
||||
Assert.That(first, Is.Not.Null);
|
||||
|
||||
// Second restore returns null because it was removed on first restore
|
||||
ContinuationPoint second = session.RestoreContinuationPoint(idBytes.ToByteString());
|
||||
ContinuationPoint second = session.ContinuationPoints.RestoreBrowse(idBytes.ToByteString());
|
||||
Assert.That(second, Is.Null);
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ namespace Opc.Ua.Server.Tests
|
||||
await CreateAndActivateAsync("SaveNullContinuation").ConfigureAwait(false);
|
||||
|
||||
Assert.That(
|
||||
() => session.SaveContinuationPoint(continuationPoint: null),
|
||||
() => session.ContinuationPoints.SaveBrowse(continuationPoint: null),
|
||||
Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
@@ -248,11 +248,11 @@ namespace Opc.Ua.Server.Tests
|
||||
await CreateAndActivateAsync("SaveRestoreHistory").ConfigureAwait(false);
|
||||
|
||||
var id = Guid.NewGuid();
|
||||
object value = new();
|
||||
session.SaveHistoryContinuationPoint(id, value);
|
||||
var value = new TestHistoryContinuationPoint(id);
|
||||
session.ContinuationPoints.SaveHistory(value);
|
||||
|
||||
byte[] idBytes = id.ToByteArray();
|
||||
object restored = session.RestoreHistoryContinuationPoint(idBytes.ToByteString());
|
||||
IHistoryContinuationPoint restored = session.ContinuationPoints.RestoreHistory(idBytes.ToByteString());
|
||||
|
||||
Assert.That(restored, Is.SameAs(value));
|
||||
}
|
||||
@@ -264,7 +264,7 @@ namespace Opc.Ua.Server.Tests
|
||||
await CreateAndActivateAsync("RestoreUnknownHistory").ConfigureAwait(false);
|
||||
|
||||
byte[] unknownId = Guid.NewGuid().ToByteArray();
|
||||
object result = session.RestoreHistoryContinuationPoint(unknownId.ToByteString());
|
||||
IHistoryContinuationPoint result = session.ContinuationPoints.RestoreHistory(unknownId.ToByteString());
|
||||
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ namespace Opc.Ua.Server.Tests
|
||||
|
||||
// Must be 16 bytes (Guid size); shorter returns null
|
||||
byte[] shortBytes = [0xAB, 0xCD];
|
||||
object result = session.RestoreHistoryContinuationPoint(shortBytes.ToByteString());
|
||||
IHistoryContinuationPoint result = session.ContinuationPoints.RestoreHistory(shortBytes.ToByteString());
|
||||
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
@@ -289,15 +289,15 @@ namespace Opc.Ua.Server.Tests
|
||||
await CreateAndActivateAsync("RestoreRemovesHistory").ConfigureAwait(false);
|
||||
|
||||
var id = Guid.NewGuid();
|
||||
session.SaveHistoryContinuationPoint(id, new object());
|
||||
session.ContinuationPoints.SaveHistory(new TestHistoryContinuationPoint(id));
|
||||
byte[] idBytes = id.ToByteArray();
|
||||
|
||||
// First restore retrieves
|
||||
object first = session.RestoreHistoryContinuationPoint(idBytes.ToByteString());
|
||||
IHistoryContinuationPoint first = session.ContinuationPoints.RestoreHistory(idBytes.ToByteString());
|
||||
Assert.That(first, Is.Not.Null);
|
||||
|
||||
// Second restore returns null
|
||||
object second = session.RestoreHistoryContinuationPoint(idBytes.ToByteString());
|
||||
IHistoryContinuationPoint second = session.ContinuationPoints.RestoreHistory(idBytes.ToByteString());
|
||||
Assert.That(second, Is.Null);
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ namespace Opc.Ua.Server.Tests
|
||||
await CreateAndActivateAsync("SaveNullHistory").ConfigureAwait(false);
|
||||
|
||||
Assert.That(
|
||||
() => session.SaveHistoryContinuationPoint(Guid.NewGuid(), continuationPoint: null),
|
||||
() => session.ContinuationPoints.SaveHistory(continuationPoint: null!),
|
||||
Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
@@ -370,11 +370,11 @@ namespace Opc.Ua.Server.Tests
|
||||
(RequestHeader requestHeader, SecureChannelContext channelCtx, ISession session) =
|
||||
await CreateAndActivateAsync("ValidateRequestWriteCount").ConfigureAwait(false);
|
||||
|
||||
uint before = session.SessionDiagnostics.WriteCount.TotalCount;
|
||||
uint before = session.ReadDiagnostics(d => d.WriteCount.TotalCount);
|
||||
|
||||
session.ValidateRequest(requestHeader, channelCtx, RequestType.Write);
|
||||
|
||||
Assert.That(session.SessionDiagnostics.WriteCount.TotalCount,
|
||||
Assert.That(session.ReadDiagnostics(d => d.WriteCount.TotalCount),
|
||||
Is.GreaterThan(before));
|
||||
}
|
||||
|
||||
@@ -384,11 +384,11 @@ namespace Opc.Ua.Server.Tests
|
||||
(RequestHeader requestHeader, SecureChannelContext channelCtx, ISession session) =
|
||||
await CreateAndActivateAsync("ValidateRequestBrowseCount").ConfigureAwait(false);
|
||||
|
||||
uint before = session.SessionDiagnostics.BrowseCount.TotalCount;
|
||||
uint before = session.ReadDiagnostics(d => d.BrowseCount.TotalCount);
|
||||
|
||||
session.ValidateRequest(requestHeader, channelCtx, RequestType.Browse);
|
||||
|
||||
Assert.That(session.SessionDiagnostics.BrowseCount.TotalCount,
|
||||
Assert.That(session.ReadDiagnostics(d => d.BrowseCount.TotalCount),
|
||||
Is.GreaterThan(before));
|
||||
}
|
||||
|
||||
@@ -398,11 +398,11 @@ namespace Opc.Ua.Server.Tests
|
||||
(RequestHeader requestHeader, SecureChannelContext channelCtx, ISession session) =
|
||||
await CreateAndActivateAsync("ValidateRequestTranslateCount").ConfigureAwait(false);
|
||||
|
||||
uint before = session.SessionDiagnostics.TranslateBrowsePathsToNodeIdsCount.TotalCount;
|
||||
uint before = session.ReadDiagnostics(d => d.TranslateBrowsePathsToNodeIdsCount.TotalCount);
|
||||
|
||||
session.ValidateRequest(requestHeader, channelCtx, RequestType.TranslateBrowsePathsToNodeIds);
|
||||
|
||||
Assert.That(session.SessionDiagnostics.TranslateBrowsePathsToNodeIdsCount.TotalCount,
|
||||
Assert.That(session.ReadDiagnostics(d => d.TranslateBrowsePathsToNodeIdsCount.TotalCount),
|
||||
Is.GreaterThan(before));
|
||||
}
|
||||
|
||||
@@ -412,11 +412,11 @@ namespace Opc.Ua.Server.Tests
|
||||
(RequestHeader requestHeader, SecureChannelContext channelCtx, ISession session) =
|
||||
await CreateAndActivateAsync("ValidateRequestTotalCount").ConfigureAwait(false);
|
||||
|
||||
uint before = session.SessionDiagnostics.TotalRequestCount.TotalCount;
|
||||
uint before = session.ReadDiagnostics(d => d.TotalRequestCount.TotalCount);
|
||||
|
||||
session.ValidateRequest(requestHeader, channelCtx, RequestType.Read);
|
||||
|
||||
Assert.That(session.SessionDiagnostics.TotalRequestCount.TotalCount,
|
||||
Assert.That(session.ReadDiagnostics(d => d.TotalRequestCount.TotalCount),
|
||||
Is.EqualTo(before + 1));
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ namespace Opc.Ua.Server.Tests
|
||||
const string sessionName = "DiagnosticsNameTest";
|
||||
(_, _, ISession session) = await CreateAndActivateAsync(sessionName).ConfigureAwait(false);
|
||||
|
||||
Assert.That(session.SessionDiagnostics.SessionName, Is.EqualTo(sessionName));
|
||||
Assert.That(session.ReadDiagnostics(d => d.SessionName), Is.EqualTo(sessionName));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -435,7 +435,7 @@ namespace Opc.Ua.Server.Tests
|
||||
(_, _, ISession session) =
|
||||
await CreateAndActivateAsync("DiagnosticsTimeout").ConfigureAwait(false);
|
||||
|
||||
Assert.That(session.SessionDiagnostics.ActualSessionTimeout, Is.GreaterThan(0.0));
|
||||
Assert.That(session.ReadDiagnostics(d => d.ActualSessionTimeout), Is.GreaterThan(0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,40 +383,6 @@ namespace Opc.Ua.Server.Tests
|
||||
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SynchronousActivationValidationRequiresAsyncPathForSecureEndpointsAsync()
|
||||
{
|
||||
EndpointDescription endpoint = CreateEndpoint(
|
||||
MessageSecurityMode.SignAndEncrypt,
|
||||
includeUserName: true);
|
||||
using SecuritySessionManager manager = CreateManager();
|
||||
CreatedSession created = await CreateSessionAsync(
|
||||
manager,
|
||||
endpoint,
|
||||
"channel-1",
|
||||
m_clientCertificate).ConfigureAwait(false);
|
||||
SignatureData signature = CreateClientSignature(
|
||||
created.Context,
|
||||
created.ClientNonce,
|
||||
created.ServerNonce,
|
||||
m_clientCertificate);
|
||||
var session = (Opc.Ua.Server.Session)created.Result.Session;
|
||||
|
||||
// The retained synchronous contract cannot verify a user token that
|
||||
// requires decryption, so it fails closed and directs callers to
|
||||
// ValidateBeforeActivateAsync instead of validating with less rigour.
|
||||
ServiceResultException exception = Assert.Throws<ServiceResultException>(
|
||||
() => session.ValidateBeforeActivate(
|
||||
created.Context,
|
||||
signature,
|
||||
CreateUserNameToken("alice"),
|
||||
null!,
|
||||
out _,
|
||||
out _))!;
|
||||
|
||||
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadNotSupported));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ActivationValidationDecodesBinaryEncodedUserIdentityTokenAsync()
|
||||
{
|
||||
|
||||
@@ -32,13 +32,13 @@ namespace Opc.Ua.Server.Tests
|
||||
server.CurrentInstance.SessionManager.SessionDiagnosticsChanged += (s, reason)
|
||||
=> eventRaised = true;
|
||||
|
||||
uint before = session.SessionDiagnostics.ReadCount.TotalCount;
|
||||
uint before = session.ReadDiagnostics(d => d.ReadCount.TotalCount);
|
||||
|
||||
// Call ValidateRequest for a request type that maps to a counter (Read).
|
||||
session.ValidateRequest(requestHeader, secureChannelContext, RequestType.Read);
|
||||
|
||||
Assert.That(eventRaised, Is.True, "SessionDiagnosticsChanged event should be raised when a per-request counter changes.");
|
||||
Assert.That(session.SessionDiagnostics.ReadCount.TotalCount, Is.GreaterThan(before), "ReadCount.TotalCount should have incremented.");
|
||||
Assert.That(session.ReadDiagnostics(d => d.ReadCount.TotalCount), Is.GreaterThan(before), "ReadCount.TotalCount should have incremented.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -80,13 +80,13 @@ namespace Opc.Ua.Server.Tests
|
||||
=> eventRaised = true;
|
||||
|
||||
// Capture total requests before; UpdateDiagnosticCounters always increments TotalRequestCount.
|
||||
uint totalBefore = session.SessionDiagnostics.TotalRequestCount.TotalCount;
|
||||
uint totalBefore = session.ReadDiagnostics(d => d.TotalRequestCount.TotalCount);
|
||||
|
||||
// Call ValidateRequest with one of the ignored request types.
|
||||
session.ValidateRequest(requestHeader, secureChannelContext, requestType);
|
||||
|
||||
Assert.That(
|
||||
session.SessionDiagnostics.TotalRequestCount.TotalCount,
|
||||
session.ReadDiagnostics(d => d.TotalRequestCount.TotalCount),
|
||||
Is.EqualTo(totalBefore + 1),
|
||||
"TotalRequestCount should increment for all request types.");
|
||||
|
||||
|
||||
@@ -75,14 +75,8 @@ namespace Opc.Ua.Server.Tests
|
||||
m_sessionMock.Setup(s => s.Id).Returns(new NodeId(Guid.NewGuid()));
|
||||
m_sessionMock.Setup(s => s.Identity).Returns(identity);
|
||||
m_sessionMock.Setup(s => s.IdentityToken).Returns(identity.TokenHandler);
|
||||
m_sessionMock.Setup(s => s.SessionDiagnostics).Returns(
|
||||
new SessionDiagnosticsDataType
|
||||
{
|
||||
ClientDescription = new ApplicationDescription
|
||||
{
|
||||
ApplicationUri = "urn:localhost:opcfoundation.org:SubscriptionLifecycleTests"
|
||||
}
|
||||
});
|
||||
m_sessionMock.Setup(s => s.ClientApplicationUri).Returns(
|
||||
"urn:localhost:opcfoundation.org:SubscriptionLifecycleTests");
|
||||
|
||||
m_diagnosticsNodeManagerMock
|
||||
.Setup(d => d.CreateSubscriptionDiagnosticsAsync(
|
||||
|
||||
@@ -87,19 +87,20 @@ namespace Opc.Ua.Server.Tests
|
||||
m_sessionMock.Setup(s => s.Id).Returns(new NodeId(Guid.NewGuid()));
|
||||
m_sessionMock.Setup(s => s.Identity).Returns(identity);
|
||||
m_sessionMock.Setup(s => s.IdentityToken).Returns(identity.TokenHandler);
|
||||
var sessionDiagnostics = new SessionDiagnosticsDataType
|
||||
{
|
||||
ClientDescription = new ApplicationDescription
|
||||
{
|
||||
ApplicationUri = "urn:localhost:opcfoundation.org:SubscriptionTests"
|
||||
}
|
||||
};
|
||||
m_sessionMock
|
||||
.Setup(s => s.UpdateDiagnostics(
|
||||
It.IsAny<Action<SessionDiagnosticsDataType>>()))
|
||||
.Callback<Action<SessionDiagnosticsDataType>>(
|
||||
update => update(m_sessionMock.Object.SessionDiagnostics));
|
||||
m_sessionMock.Setup(s => s.SessionDiagnostics).Returns(
|
||||
new SessionDiagnosticsDataType
|
||||
{
|
||||
ClientDescription = new ApplicationDescription
|
||||
{
|
||||
ApplicationUri = "urn:localhost:opcfoundation.org:SubscriptionTests"
|
||||
}
|
||||
});
|
||||
update => update(sessionDiagnostics));
|
||||
m_sessionMock.Setup(s => s.ClientApplicationUri)
|
||||
.Returns(() => sessionDiagnostics.ClientDescription?.ApplicationUri);
|
||||
|
||||
m_diagnosticsNodeManagerMock
|
||||
.Setup(d => d.CreateSubscriptionDiagnosticsAsync(
|
||||
@@ -371,19 +372,20 @@ namespace Opc.Ua.Server.Tests
|
||||
var identity = new UserIdentity(tokenHandler);
|
||||
session.Setup(s => s.Identity).Returns(identity);
|
||||
session.Setup(s => s.IdentityToken).Returns(tokenHandler);
|
||||
var diagnostics = new SessionDiagnosticsDataType
|
||||
{
|
||||
ClientDescription = new ApplicationDescription
|
||||
{
|
||||
ApplicationUri = "urn:localhost:opcfoundation.org:SubscriptionTests"
|
||||
}
|
||||
};
|
||||
session
|
||||
.Setup(s => s.UpdateDiagnostics(
|
||||
It.IsAny<Action<SessionDiagnosticsDataType>>()))
|
||||
.Callback<Action<SessionDiagnosticsDataType>>(
|
||||
update => update(session.Object.SessionDiagnostics));
|
||||
session.Setup(s => s.SessionDiagnostics).Returns(
|
||||
new SessionDiagnosticsDataType
|
||||
{
|
||||
ClientDescription = new ApplicationDescription
|
||||
{
|
||||
ApplicationUri = "urn:localhost:opcfoundation.org:SubscriptionTests"
|
||||
}
|
||||
});
|
||||
update => update(diagnostics));
|
||||
session.Setup(s => s.ClientApplicationUri)
|
||||
.Returns(() => diagnostics.ClientDescription?.ApplicationUri);
|
||||
}
|
||||
|
||||
private static void SetExpiryTime(Subscription subscription, long expiryTime)
|
||||
@@ -475,8 +477,8 @@ namespace Opc.Ua.Server.Tests
|
||||
destinationSession.SetupGet(session => session.IdentityToken)
|
||||
.Returns(identity.TokenHandler);
|
||||
SessionDiagnosticsDataType destinationDiagnostics = CreateSessionDiagnostics();
|
||||
destinationSession.SetupGet(session => session.SessionDiagnostics)
|
||||
.Returns(destinationDiagnostics);
|
||||
destinationSession.SetupGet(session => session.ClientApplicationUri)
|
||||
.Returns(() => destinationDiagnostics.ClientDescription?.ApplicationUri);
|
||||
destinationSession
|
||||
.Setup(session => session.UpdateDiagnostics(
|
||||
It.IsAny<Action<SessionDiagnosticsDataType>>()))
|
||||
@@ -1066,8 +1068,8 @@ namespace Opc.Ua.Server.Tests
|
||||
destinationSession.SetupGet(session => session.IdentityToken)
|
||||
.Returns(identity.TokenHandler);
|
||||
SessionDiagnosticsDataType destinationDiagnostics = CreateSessionDiagnostics();
|
||||
destinationSession.SetupGet(session => session.SessionDiagnostics)
|
||||
.Returns(destinationDiagnostics);
|
||||
destinationSession.SetupGet(session => session.ClientApplicationUri)
|
||||
.Returns(() => destinationDiagnostics.ClientDescription?.ApplicationUri);
|
||||
destinationSession
|
||||
.Setup(session => session.UpdateDiagnostics(
|
||||
It.IsAny<Action<SessionDiagnosticsDataType>>()))
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
||||
*
|
||||
* OPC Foundation MIT License 1.00
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* The complete license agreement can be found here:
|
||||
* http://opcfoundation.org/License/MIT/1.00/
|
||||
* ======================================================================*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Opc.Ua.Server.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// A history continuation point that carries nothing but its identity, and records
|
||||
/// whether the session disposed it.
|
||||
/// </summary>
|
||||
internal sealed class TestHistoryContinuationPoint : IHistoryContinuationPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a point with the given identity, or a fresh one.
|
||||
/// </summary>
|
||||
/// <param name="id">The identity to use.</param>
|
||||
public TestHistoryContinuationPoint(Guid? id = null)
|
||||
{
|
||||
Id = id ?? Guid.NewGuid();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the owner disposed this point.
|
||||
/// </summary>
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
Disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,14 +201,7 @@ namespace Opc.Ua.Subscriptions.Tests
|
||||
session.SetupGet(value => value.Id).Returns(new NodeId(Guid.NewGuid()));
|
||||
session.SetupGet(value => value.Identity).Returns(new UserIdentity());
|
||||
session.SetupGet(value => value.IdentityToken).Returns(CreateIdentityToken().Object);
|
||||
session.SetupGet(value => value.SessionDiagnostics).Returns(
|
||||
new SessionDiagnosticsDataType
|
||||
{
|
||||
ClientDescription = new ApplicationDescription
|
||||
{
|
||||
ApplicationUri = "urn:test"
|
||||
}
|
||||
});
|
||||
session.SetupGet(value => value.ClientApplicationUri).Returns("urn:test");
|
||||
session.Setup(value => value.IsSecureChannelValid(It.IsAny<string>())).Returns(true);
|
||||
|
||||
return new SessionPublishQueue(server.Object, session.Object, 10, TimeProvider.System);
|
||||
|
||||
Reference in New Issue
Block a user