Files
UA-.NETStandard/tests/Opc.Ua.Core.Tests/Stack/Server/SessionBindingContextTests.cs
Marc Schier 57c0a32ed4 Fix incomplete-message buffering and channel cleanup (#4522)
# Description

Fix incomplete-message buffering, channel cleanup, session resource
accounting, and enforcement of existing transport admission settings.

- Share a targeted `ChunkReassemblyBudget` across server listeners. The
reference default is 64 MiB, with a 32 MiB global occupancy threshold
for sessionless intermediate chunks. This is priority headroom for
activated sessions, not a guaranteed bootstrap partition. Single/final
chunks and unrelated response/client buffers remain outside this budget.
- Retain precise incoming/final payload quotas, valid Abort handling
without channel teardown, terminal late-final rejection, and
exactly-once buffer/reservation cleanup.
- Use one assembly timer per quota/clock scope, asynchronous cleanup, a
fixed deadline, a 30-second fallback for non-positive lifetimes, and
best-effort ERR `BadTimeout` before closure.
- Apply negotiated TCP/WebSocket/Pipe receive limits and size pipe
rentals to actual chunks.
- Add the optional `ISessionBindingProvider` and immutable
`SessionBindingContext` seam. Managed membership counts distinct
committed sessions, not responses, and handles reactivation, transfer,
closure, expiry, restoration and shutdown. Read-only snapshots require
token/channel/security validation and never replace service
authorization.
- Wire default and explicitly injected binding providers through raw
TCP, Kestrel and UACP WebSockets; preserve existing callback/manager
interfaces and direct construction.
- Enforce existing `ConnectionRateLimiter` and channel capacity settings
on Kestrel TCP and UACP WebSocket admission, with physical-transport
leases surviving reverse handoff and releasing on cancellation, close,
failed attachment and stop. HTTP/JSON requests are not treated as UASC
channels.
- Add regression coverage and documentation. Stabilize the CI
cancellation test by waiting for the resolver to enter before canceling.

This is the foundation only. New Balanced/FairShare/TrustedReservations
policies, tenant isolation and guaranteed bootstrap reserves belong in
the separate stacked follow-up, not this PR.

## Related Issues

Fixes #4521.

## Validation

| Scope | Result |
| --- | --- |
| Core transport, budget, binding-context and DI tests, net10.0 | 717
passed |
| Core transport, budget, binding-context and DI tests, net48 | 664
passed, 6 platform-specific skips |
| Session bindings, transfer/security/expiry/restore and hosted DI,
net10.0 | 69 passed |
| Same focused Server suite, net48 | 69 passed |
| ServerRedundancyHandlerTests, Release/net48 | 39 passed |
| ConsoleReferenceServer net10.0 | Build succeeded, zero warnings/errors
|
| Server and HTTPS binding netstandard2.1 | Both builds succeeded, zero
warnings/errors |

The new admission fixtures also passed five repeated runs after
correcting asynchronous connection-context disposal. Full-solution
validation, coverage comparison and CI for this update remain pending.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-09-24 20:12:55 +02:00

88 lines
4.1 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 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/
* ======================================================================*/
#nullable enable
using System;
using NUnit.Framework;
namespace Opc.Ua.Core.Tests.Stack.Server
{
[TestFixture]
public sealed class SessionBindingContextTests
{
[TestCase(UserTokenType.Anonymous, null)]
[TestCase(UserTokenType.UserName, "user")]
[TestCase(UserTokenType.Certificate, "certificate")]
[TestCase(UserTokenType.IssuedToken, "issuer:subject")]
public void SnapshotPreservesValidatedValues(UserTokenType tokenType, string? userId)
{
var context = new SessionBindingContext(
new NodeId(1), "channel", 7, tokenType, userId,
SecurityPolicies.Basic256Sha256, MessageSecurityMode.SignAndEncrypt);
Assert.That(context.SessionId, Is.EqualTo(new NodeId(1)));
Assert.That(context.SecureChannelId, Is.EqualTo("channel"));
Assert.That(context.ActivationSequence, Is.EqualTo(7));
Assert.That(context.UserTokenType, Is.EqualTo(tokenType));
Assert.That(context.ClientUserId, Is.EqualTo(userId));
Assert.That(context.SecurityPolicyUri, Is.EqualTo(SecurityPolicies.Basic256Sha256));
Assert.That(context.SecurityMode, Is.EqualTo(MessageSecurityMode.SignAndEncrypt));
}
[TestCase("sessionId")]
[TestCase("secureChannelId")]
[TestCase("activationSequence")]
[TestCase("userTokenType")]
[TestCase("clientUserId")]
[TestCase("securityPolicyUri")]
[TestCase("securityMode")]
public void SnapshotRejectsInvalidState(string parameter)
{
Assert.That(() => new SessionBindingContext(
parameter == "sessionId" ? NodeId.Null : new NodeId(1),
parameter == "secureChannelId" ? string.Empty : "channel",
parameter == "activationSequence" ? 0 : 1,
parameter == "userTokenType" ? (UserTokenType)99 : UserTokenType.Anonymous,
parameter == "clientUserId" ? "unvalidated-user" : null,
parameter == "securityPolicyUri" ? string.Empty : SecurityPolicies.None,
parameter == "securityMode" ? MessageSecurityMode.Invalid : MessageSecurityMode.None),
Throws.InstanceOf<ArgumentException>().With.Property("ParamName").EqualTo(parameter));
}
[Test]
public void AuthenticatedSnapshotRequiresContinuityKey()
{
Assert.That(() => new SessionBindingContext(
new NodeId(1), "channel", 1, UserTokenType.UserName, null,
SecurityPolicies.None, MessageSecurityMode.None),
Throws.ArgumentException.With.Property("ParamName").EqualTo("clientUserId"));
}
}
}