fix(server): resolve Low code-review findings (Server-004,006,008,012,014,015)

- Server-004: pass the role-derived display name to UserIdentity's base
  ctor (the SDK's DisplayName has no public setter) and drop the dead
  Display property; make RoleBasedIdentity internal sealed.
- Server-006: derive a bounded CancellationToken from the SDK's
  OperationContext.OperationDeadline in OnReadValue / OnWriteValue so a
  stalled driver call can no longer pin the request thread.
- Server-008: mark handled slots via CallMethodRequest.Processed = true
  in RouteScriptedAlarmMethodCalls (the SDK skips on Processed, not on a
  Good error slot).
- Server-012: PeerHttpProbeLoop.ProbeAsync stops mutating client.Timeout
  per call; uses a per-request CancellationTokenSource linked to the
  shutdown token instead.
- Server-014: wire SealedBootstrap into Program.cs via AddSealedBootstrap
  + OpcUaServerService so the generation-sealed cache + stale-config flag
  + resilient reader actually run; /healthz now reflects cache-fallback
  state.
- Server-015: replace the stale 'PR 16 / PR 17 minimum-viable scope'
  class summaries on OtOpcUaServer and OpcUaServerOptions with the
  shipped LDAP + anonymous-role + configurable security-profile prose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-23 07:24:20 -04:00
parent 2b33b64a58
commit 6134050ceb
14 changed files with 698 additions and 40 deletions

View File

@@ -0,0 +1,56 @@
using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
/// <summary>
/// Regression for Server-004 — the production
/// <see cref="OtOpcUaServer.RoleBasedIdentity"/> must surface the LDAP-resolved display
/// name through <see cref="IUserIdentity.DisplayName"/>, since
/// <c>DriverNodeManager.ResolveCallUser</c> reads the base interface property when stamping
/// audit identities on scripted-alarm Acknowledge / Confirm / Shelve calls.
/// </summary>
[Trait("Category", "Unit")]
public sealed class RoleBasedIdentityTests
{
[Fact]
public void DisplayName_returns_LDAP_resolved_display_name_when_present()
{
IUserIdentity identity = new OtOpcUaServer.RoleBasedIdentity(
userName: "alice",
displayName: "Alice Smith",
roles: new[] { "WriteOperate" },
ldapGroups: new[] { "ot_operators" });
identity.DisplayName.ShouldBe("Alice Smith",
"DriverNodeManager.ResolveCallUser reads IUserIdentity.DisplayName for audit entries; "
+ "RoleBasedIdentity must surface the LDAP-resolved name, not just the username.");
}
[Fact]
public void DisplayName_falls_back_to_userName_when_LDAP_display_name_is_null()
{
IUserIdentity identity = new OtOpcUaServer.RoleBasedIdentity(
userName: "alice",
displayName: null,
roles: [],
ldapGroups: []);
identity.DisplayName.ShouldBe("alice",
"absent an LDAP display name, audit entries should still carry the username.");
}
[Fact]
public void ResolveCallUser_yields_LDAP_resolved_display_name()
{
IUserIdentity identity = new OtOpcUaServer.RoleBasedIdentity(
userName: "alice",
displayName: "Alice Smith",
roles: [],
ldapGroups: []);
DriverNodeManager.ResolveCallUser(identity).ShouldBe("Alice Smith");
}
}