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:
Marc Schier
2026-08-11 06:57:40 +02:00
committed by GitHub
parent 9375a2431b
commit 5bc8a9d705
31 changed files with 555 additions and 445 deletions
+70
View File
@@ -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;
}
}
+10 -1
View File
@@ -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; }
}
}
+11 -57
View File
@@ -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);
}
}
+19 -78
View File
@@ -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;
}
+2 -2
View File
@@ -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();
}
}
@@ -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()
{
+4 -4
View File
@@ -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(
+24 -22
View File
@@ -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);