diff --git a/NatsDotNet.slnx b/NatsDotNet.slnx index 5dc31f2..7fed1de 100644 --- a/NatsDotNet.slnx +++ b/NatsDotNet.slnx @@ -6,6 +6,7 @@ + diff --git a/src/NATS.Server/NATS.Server.csproj b/src/NATS.Server/NATS.Server.csproj index 8b5edc5..719e71b 100644 --- a/src/NATS.Server/NATS.Server.csproj +++ b/src/NATS.Server/NATS.Server.csproj @@ -1,6 +1,7 @@ + diff --git a/tests/NATS.Server.TestUtilities/SocketTestHelper.cs b/tests/NATS.Server.TestUtilities/SocketTestHelper.cs index fbb3ee6..867cd10 100644 --- a/tests/NATS.Server.TestUtilities/SocketTestHelper.cs +++ b/tests/NATS.Server.TestUtilities/SocketTestHelper.cs @@ -18,4 +18,18 @@ public static class SocketTestHelper } return sb.ToString(); } + + public static async Task ReadUntilAsync(Stream stream, string expected, int timeoutMs = 5000) + { + using var cts = new CancellationTokenSource(timeoutMs); + var sb = new StringBuilder(); + var buf = new byte[4096]; + while (!sb.ToString().Contains(expected, StringComparison.Ordinal)) + { + var n = await stream.ReadAsync(buf, cts.Token); + if (n == 0) break; + sb.Append(Encoding.ASCII.GetString(buf, 0, n)); + } + return sb.ToString(); + } } diff --git a/tests/NATS.Server.TestUtilities/TestCertHelper.cs b/tests/NATS.Server.TestUtilities/TestCertHelper.cs new file mode 100644 index 0000000..c3acb55 --- /dev/null +++ b/tests/NATS.Server.TestUtilities/TestCertHelper.cs @@ -0,0 +1,31 @@ +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace NATS.Server.TestUtilities; + +public static class TestCertHelper +{ + public static (string certPath, string keyPath) GenerateTestCertFiles() + { + var (cert, key) = GenerateTestCert(); + var certPath = Path.GetTempFileName(); + var keyPath = Path.GetTempFileName(); + File.WriteAllText(certPath, cert.ExportCertificatePem()); + File.WriteAllText(keyPath, key.ExportPkcs8PrivateKeyPem()); + return (certPath, keyPath); + } + + public static (X509Certificate2 cert, RSA key) GenerateTestCert() + { + var key = RSA.Create(2048); + var req = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddDnsName("localhost"); + req.CertificateExtensions.Add(sanBuilder.Build()); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); + return (cert, key); + } +} diff --git a/tests/NATS.Server.Tests/MonitorTests.cs b/tests/NATS.Server.Tests/MonitorTests.cs index bb1356a..0993c41 100644 --- a/tests/NATS.Server.Tests/MonitorTests.cs +++ b/tests/NATS.Server.Tests/MonitorTests.cs @@ -5,6 +5,7 @@ using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server.Monitoring; +using NATS.Server.TestUtilities; namespace NATS.Server.Tests; @@ -310,7 +311,7 @@ public class MonitorTlsTests : IAsyncLifetime { _natsPort = GetFreePort(); _monitorPort = GetFreePort(); - (_certPath, _keyPath) = TlsHelperTests.GenerateTestCertFiles(); + (_certPath, _keyPath) = TestCertHelper.GenerateTestCertFiles(); _server = new NatsServer( new NatsOptions { diff --git a/tests/NATS.Server.Tests/MsgTraceGoParityTests.cs b/tests/NATS.Server.Tests/MsgTraceGoParityTests.cs index 41afc82..00aa7e7 100644 --- a/tests/NATS.Server.Tests/MsgTraceGoParityTests.cs +++ b/tests/NATS.Server.Tests/MsgTraceGoParityTests.cs @@ -15,6 +15,7 @@ using NATS.Server; using NATS.Server.Auth; using NATS.Server.Monitoring; using NATS.Server.Protocol; +using NATS.Server.TestUtilities; namespace NATS.Server.Tests; @@ -784,7 +785,7 @@ public class MsgTraceGoParityTests : IAsyncLifetime public async Task ClosedConns_tls_handshake_close_reason_tracked() { // Go: TestClosedTLSHandshake (closed_conns_test.go:247) - var (certPath, keyPath) = TlsHelperTests.GenerateTestCertFiles(); + var (certPath, keyPath) = TestCertHelper.GenerateTestCertFiles(); try { var port = GetFreePort(); diff --git a/tests/NATS.Server.Tests/IO/AdaptiveReadBufferShortReadTests.cs b/tests/NATS.Server.Transport.Tests/IO/AdaptiveReadBufferShortReadTests.cs similarity index 99% rename from tests/NATS.Server.Tests/IO/AdaptiveReadBufferShortReadTests.cs rename to tests/NATS.Server.Transport.Tests/IO/AdaptiveReadBufferShortReadTests.cs index 658e6ac..ea0f387 100644 --- a/tests/NATS.Server.Tests/IO/AdaptiveReadBufferShortReadTests.cs +++ b/tests/NATS.Server.Transport.Tests/IO/AdaptiveReadBufferShortReadTests.cs @@ -1,7 +1,7 @@ using NATS.Server.IO; using Shouldly; -namespace NATS.Server.Tests.IO; +namespace NATS.Server.Transport.Tests.IO; /// /// Tests for the consecutive short-read counter in AdaptiveReadBuffer. diff --git a/tests/NATS.Server.Tests/IO/AdaptiveReadBufferTests.cs b/tests/NATS.Server.Transport.Tests/IO/AdaptiveReadBufferTests.cs similarity index 91% rename from tests/NATS.Server.Tests/IO/AdaptiveReadBufferTests.cs rename to tests/NATS.Server.Transport.Tests/IO/AdaptiveReadBufferTests.cs index 945dc64..64a881e 100644 --- a/tests/NATS.Server.Tests/IO/AdaptiveReadBufferTests.cs +++ b/tests/NATS.Server.Transport.Tests/IO/AdaptiveReadBufferTests.cs @@ -1,6 +1,6 @@ using NATS.Server.IO; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class AdaptiveReadBufferTests { diff --git a/tests/NATS.Server.Tests/IO/DynamicBufferPoolTests.cs b/tests/NATS.Server.Transport.Tests/IO/DynamicBufferPoolTests.cs similarity index 99% rename from tests/NATS.Server.Tests/IO/DynamicBufferPoolTests.cs rename to tests/NATS.Server.Transport.Tests/IO/DynamicBufferPoolTests.cs index 92ca8fd..fc3af5f 100644 --- a/tests/NATS.Server.Tests/IO/DynamicBufferPoolTests.cs +++ b/tests/NATS.Server.Transport.Tests/IO/DynamicBufferPoolTests.cs @@ -4,7 +4,7 @@ using Shouldly; // Go reference: client.go — dynamic buffer sizing and broadcast flush coalescing for fan-out. -namespace NATS.Server.Tests.IO; +namespace NATS.Server.Transport.Tests.IO; public class DynamicBufferPoolTests { diff --git a/tests/NATS.Server.Tests/IO/OutboundBufferPoolTests.cs b/tests/NATS.Server.Transport.Tests/IO/OutboundBufferPoolTests.cs similarity index 91% rename from tests/NATS.Server.Tests/IO/OutboundBufferPoolTests.cs rename to tests/NATS.Server.Transport.Tests/IO/OutboundBufferPoolTests.cs index e5a0b23..66fc325 100644 --- a/tests/NATS.Server.Tests/IO/OutboundBufferPoolTests.cs +++ b/tests/NATS.Server.Transport.Tests/IO/OutboundBufferPoolTests.cs @@ -1,6 +1,6 @@ using NATS.Server.IO; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class OutboundBufferPoolTests { diff --git a/tests/NATS.Server.Transport.Tests/NATS.Server.Transport.Tests.csproj b/tests/NATS.Server.Transport.Tests/NATS.Server.Transport.Tests.csproj new file mode 100644 index 0000000..da6c6f1 --- /dev/null +++ b/tests/NATS.Server.Transport.Tests/NATS.Server.Transport.Tests.csproj @@ -0,0 +1,26 @@ + + + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/NATS.Server.Tests/Networking/NetworkingGoParityTests.cs b/tests/NATS.Server.Transport.Tests/Networking/NetworkingGoParityTests.cs similarity index 91% rename from tests/NATS.Server.Tests/Networking/NetworkingGoParityTests.cs rename to tests/NATS.Server.Transport.Tests/Networking/NetworkingGoParityTests.cs index bd72983..cda7cff 100644 --- a/tests/NATS.Server.Tests/Networking/NetworkingGoParityTests.cs +++ b/tests/NATS.Server.Transport.Tests/Networking/NetworkingGoParityTests.cs @@ -10,6 +10,7 @@ using NATS.Server.Gateways; using NATS.Server.LeafNodes; using NATS.Server.Routes; using NATS.Server.Subscriptions; +using NATS.Server.TestUtilities; namespace NATS.Server.Tests.Networking; @@ -177,9 +178,7 @@ public class NetworkingGoParityTests await using var sub = await conn.SubscribeCoreAsync("gw.interest.test"); await conn.PingAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.interest.test")) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.interest.test")); fixture.Local.HasRemoteInterest("gw.interest.test").ShouldBeTrue(); } @@ -204,9 +203,7 @@ public class NetworkingGoParityTests await using var sub = await remoteConn.SubscribeCoreAsync("gw.fwd.test"); await remoteConn.PingAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.fwd.test")) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.fwd.test")); await localConn.PublishAsync("gw.fwd.test", "gateway-msg"); @@ -229,18 +226,14 @@ public class NetworkingGoParityTests var sub = await conn.SubscribeCoreAsync("gw.unsub.test"); await conn.PingAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.unsub.test")) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.unsub.test")); fixture.Local.HasRemoteInterest("gw.unsub.test").ShouldBeTrue(); await sub.DisposeAsync(); await conn.PingAsync(); - using var unsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!unsTimeout.IsCancellationRequested && fixture.Local.HasRemoteInterest("gw.unsub.test")) - await Task.Delay(50, unsTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => !(fixture.Local.HasRemoteInterest("gw.unsub.test"))); fixture.Local.HasRemoteInterest("gw.unsub.test").ShouldBeFalse(); } @@ -260,9 +253,7 @@ public class NetworkingGoParityTests await using var sub = await conn.SubscribeCoreAsync("gw.wild.>"); await conn.PingAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.wild.test")) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.wild.test")); fixture.Local.HasRemoteInterest("gw.wild.test").ShouldBeTrue(); fixture.Local.HasRemoteInterest("gw.wild.deep.nested").ShouldBeTrue(); @@ -410,9 +401,7 @@ public class NetworkingGoParityTests try { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && serverA.Stats.Routes < 3) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3)); serverA.Stats.Routes.ShouldBeGreaterThanOrEqualTo(3); } @@ -488,9 +477,7 @@ public class NetworkingGoParityTests try { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && serverA.Stats.Routes < 3) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3)); await using var conn = new NatsConnection(new NatsOpts { @@ -501,9 +488,7 @@ public class NetworkingGoParityTests await using var sub = await conn.SubscribeCoreAsync("route.sub.test"); await conn.PingAsync(); - using var interest = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!interest.IsCancellationRequested && !serverA.HasRemoteInterest("route.sub.test")) - await Task.Delay(50, interest.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => serverA.HasRemoteInterest("route.sub.test")); serverA.HasRemoteInterest("route.sub.test").ShouldBeTrue(); } @@ -582,9 +567,7 @@ public class NetworkingGoParityTests try { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && serverA.Stats.Routes < 3) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3)); await using var subConn = new NatsConnection(new NatsOpts { @@ -601,9 +584,7 @@ public class NetworkingGoParityTests await using var sub = await subConn.SubscribeCoreAsync("route.fwd.test"); await subConn.PingAsync(); - using var interest = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!interest.IsCancellationRequested && !serverA.HasRemoteInterest("route.fwd.test")) - await Task.Delay(50, interest.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => serverA.HasRemoteInterest("route.fwd.test")); await pubConn.PublishAsync("route.fwd.test", "routed-msg"); @@ -852,7 +833,7 @@ public class NetworkingGoParityTests leaf.StartLoop(cts.Token); await WriteLineAsync(remoteSocket, "LS+ $G events.>", cts.Token); - await Task.Delay(100); + await PollHelper.YieldForAsync(100); await WriteLineAsync(remoteSocket, "LS- $G events.>", cts.Token); var result = await received.Task.WaitAsync(cts.Token); @@ -1132,9 +1113,7 @@ internal sealed class TwoGatewayFixture : IAsyncDisposable _ = remote.StartAsync(remoteCts.Token); await remote.WaitForReadyAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && (local.Stats.Gateways == 0 || remote.Stats.Gateways == 0)) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => !((local.Stats.Gateways == 0 || remote.Stats.Gateways == 0))); return new TwoGatewayFixture(local, remote, localCts, remoteCts); } @@ -1205,37 +1184,23 @@ internal sealed class LeafFixture : IAsyncDisposable _ = spoke.StartAsync(spokeCts.Token); await spoke.WaitForReadyAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested && (hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0)) - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); + await PollHelper.WaitUntilAsync(() => !((hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0))); return new LeafFixture(hub, spoke, hubCts, spokeCts); } public async Task WaitForRemoteInterestOnHubAsync(string subject) { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested) - { - if (Hub.HasRemoteInterest(subject)) - return; - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); - } - - throw new TimeoutException($"Timed out waiting for remote interest on hub for '{subject}'."); + await PollHelper.WaitOrThrowAsync( + () => Hub.HasRemoteInterest(subject), + $"Timed out waiting for remote interest on hub for '{subject}'."); } public async Task WaitForRemoteInterestOnSpokeAsync(string subject) { - using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - while (!timeout.IsCancellationRequested) - { - if (Spoke.HasRemoteInterest(subject)) - return; - await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); - } - - throw new TimeoutException($"Timed out waiting for remote interest on spoke for '{subject}'."); + await PollHelper.WaitOrThrowAsync( + () => Spoke.HasRemoteInterest(subject), + $"Timed out waiting for remote interest on spoke for '{subject}'."); } public async ValueTask DisposeAsync() diff --git a/tests/NATS.Server.Tests/OcspConfigTests.cs b/tests/NATS.Server.Transport.Tests/OcspConfigTests.cs similarity index 98% rename from tests/NATS.Server.Tests/OcspConfigTests.cs rename to tests/NATS.Server.Transport.Tests/OcspConfigTests.cs index 42122ac..8639f84 100644 --- a/tests/NATS.Server.Tests/OcspConfigTests.cs +++ b/tests/NATS.Server.Transport.Tests/OcspConfigTests.cs @@ -1,6 +1,6 @@ using NATS.Server.Tls; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class OcspConfigTests { diff --git a/tests/NATS.Server.Tests/OcspStaplingTests.cs b/tests/NATS.Server.Transport.Tests/OcspStaplingTests.cs similarity index 98% rename from tests/NATS.Server.Tests/OcspStaplingTests.cs rename to tests/NATS.Server.Transport.Tests/OcspStaplingTests.cs index 9c19dc0..f959d4d 100644 --- a/tests/NATS.Server.Tests/OcspStaplingTests.cs +++ b/tests/NATS.Server.Transport.Tests/OcspStaplingTests.cs @@ -1,6 +1,6 @@ using NATS.Server.Tls; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class OcspStaplingTests { diff --git a/tests/NATS.Server.Tests/TlsConnectionWrapperTests.cs b/tests/NATS.Server.Transport.Tests/TlsConnectionWrapperTests.cs similarity index 96% rename from tests/NATS.Server.Tests/TlsConnectionWrapperTests.cs rename to tests/NATS.Server.Transport.Tests/TlsConnectionWrapperTests.cs index 55df6cc..de92fc5 100644 --- a/tests/NATS.Server.Tests/TlsConnectionWrapperTests.cs +++ b/tests/NATS.Server.Transport.Tests/TlsConnectionWrapperTests.cs @@ -6,9 +6,10 @@ using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server; using NATS.Server.Protocol; +using NATS.Server.TestUtilities; using NATS.Server.Tls; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class TlsConnectionWrapperTests { @@ -32,7 +33,7 @@ public class TlsConnectionWrapperTests [Fact] public async Task TlsRequired_upgrades_to_ssl() { - var (cert, _) = TlsHelperTests.GenerateTestCert(); + var (cert, _) = TestCertHelper.GenerateTestCert(); var (serverSocket, clientSocket) = await CreateSocketPairAsync(); using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true); @@ -85,7 +86,7 @@ public class TlsConnectionWrapperTests [Fact] public async Task MixedMode_allows_plaintext_when_AllowNonTls() { - var (cert, _) = TlsHelperTests.GenerateTestCert(); + var (cert, _) = TestCertHelper.GenerateTestCert(); var (serverSocket, clientSocket) = await CreateSocketPairAsync(); using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true); @@ -133,7 +134,7 @@ public class TlsConnectionWrapperTests [Fact] public async Task TlsRequired_rejects_plaintext() { - var (cert, _) = TlsHelperTests.GenerateTestCert(); + var (cert, _) = TestCertHelper.GenerateTestCert(); var (serverSocket, clientSocket) = await CreateSocketPairAsync(); using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true); @@ -180,7 +181,7 @@ public class TlsConnectionWrapperTests [Fact] public async Task TlsFirst_handshakes_before_sending_info() { - var (cert, _) = TlsHelperTests.GenerateTestCert(); + var (cert, _) = TestCertHelper.GenerateTestCert(); var (serverSocket, clientSocket) = await CreateSocketPairAsync(); using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true); diff --git a/tests/NATS.Server.Tests/TlsHelperTests.cs b/tests/NATS.Server.Transport.Tests/TlsHelperTests.cs similarity index 76% rename from tests/NATS.Server.Tests/TlsHelperTests.cs rename to tests/NATS.Server.Transport.Tests/TlsHelperTests.cs index 7394f21..52c340b 100644 --- a/tests/NATS.Server.Tests/TlsHelperTests.cs +++ b/tests/NATS.Server.Transport.Tests/TlsHelperTests.cs @@ -2,16 +2,17 @@ using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using NATS.Server; +using NATS.Server.TestUtilities; using NATS.Server.Tls; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class TlsHelperTests { [Fact] public void LoadCertificate_loads_pem_cert_and_key() { - var (certPath, keyPath) = GenerateTestCertFiles(); + var (certPath, keyPath) = TestCertHelper.GenerateTestCertFiles(); try { var cert = TlsHelper.LoadCertificate(certPath, keyPath); @@ -24,7 +25,7 @@ public class TlsHelperTests [Fact] public void BuildServerAuthOptions_creates_valid_options() { - var (certPath, keyPath) = GenerateTestCertFiles(); + var (certPath, keyPath) = TestCertHelper.GenerateTestCertFiles(); try { var opts = new NatsOptions { TlsCert = certPath, TlsKey = keyPath }; @@ -38,7 +39,7 @@ public class TlsHelperTests [Fact] public void LoadCaCertificates_rejects_non_certificate_pem_block() { - var (_, key) = GenerateTestCert(); + var (_, key) = TestCertHelper.GenerateTestCert(); var pemPath = Path.GetTempFileName(); try { @@ -123,27 +124,10 @@ public class TlsHelperTests await limiter.WaitAsync(cts.Token); } - // Public helper methods used by other test classes + // Delegate to shared TestCertHelper in TestUtilities public static (string certPath, string keyPath) GenerateTestCertFiles() - { - var (cert, key) = GenerateTestCert(); - var certPath = Path.GetTempFileName(); - var keyPath = Path.GetTempFileName(); - File.WriteAllText(certPath, cert.ExportCertificatePem()); - File.WriteAllText(keyPath, key.ExportPkcs8PrivateKeyPem()); - return (certPath, keyPath); - } + => TestCertHelper.GenerateTestCertFiles(); public static (X509Certificate2 cert, RSA key) GenerateTestCert() - { - var key = RSA.Create(2048); - var req = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); - var sanBuilder = new SubjectAlternativeNameBuilder(); - sanBuilder.AddIpAddress(IPAddress.Loopback); - sanBuilder.AddDnsName("localhost"); - req.CertificateExtensions.Add(sanBuilder.Build()); - var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); - return (cert, key); - } + => TestCertHelper.GenerateTestCert(); } diff --git a/tests/NATS.Server.Tests/TlsMapAuthenticatorTests.cs b/tests/NATS.Server.Transport.Tests/TlsMapAuthenticatorTests.cs similarity index 98% rename from tests/NATS.Server.Tests/TlsMapAuthenticatorTests.cs rename to tests/NATS.Server.Transport.Tests/TlsMapAuthenticatorTests.cs index e3e27ed..605a9b0 100644 --- a/tests/NATS.Server.Tests/TlsMapAuthenticatorTests.cs +++ b/tests/NATS.Server.Transport.Tests/TlsMapAuthenticatorTests.cs @@ -2,7 +2,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using NATS.Server.Auth; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class TlsMapAuthenticatorTests { diff --git a/tests/NATS.Server.Tests/TlsOcspParityBatch1Tests.cs b/tests/NATS.Server.Transport.Tests/TlsOcspParityBatch1Tests.cs similarity index 95% rename from tests/NATS.Server.Tests/TlsOcspParityBatch1Tests.cs rename to tests/NATS.Server.Transport.Tests/TlsOcspParityBatch1Tests.cs index 0bdc881..98b50c2 100644 --- a/tests/NATS.Server.Tests/TlsOcspParityBatch1Tests.cs +++ b/tests/NATS.Server.Transport.Tests/TlsOcspParityBatch1Tests.cs @@ -1,9 +1,10 @@ using System.Security.Cryptography; using System.Text.Json; using NATS.Server.Configuration; +using NATS.Server.TestUtilities; using NATS.Server.Tls; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class TlsOcspParityBatch1Tests { @@ -84,7 +85,7 @@ public class TlsOcspParityBatch1Tests [Fact] public void GenerateFingerprint_uses_raw_certificate_sha256() { - var (cert, _) = TlsHelperTests.GenerateTestCert(); + var (cert, _) = TestCertHelper.GenerateTestCert(); var expected = Convert.ToBase64String(SHA256.HashData(cert.RawData)); TlsHelper.GenerateFingerprint(cert).ShouldBe(expected); @@ -104,7 +105,7 @@ public class TlsOcspParityBatch1Tests [Fact] public void Subject_and_issuer_dn_helpers_return_values_and_empty_for_null() { - var (cert, _) = TlsHelperTests.GenerateTestCert(); + var (cert, _) = TestCertHelper.GenerateTestCert(); TlsHelper.GetSubjectDNForm(cert).ShouldNotBeNullOrWhiteSpace(); TlsHelper.GetIssuerDNForm(cert).ShouldNotBeNullOrWhiteSpace(); diff --git a/tests/NATS.Server.Tests/TlsOcspParityBatch2Tests.cs b/tests/NATS.Server.Transport.Tests/TlsOcspParityBatch2Tests.cs similarity index 98% rename from tests/NATS.Server.Tests/TlsOcspParityBatch2Tests.cs rename to tests/NATS.Server.Transport.Tests/TlsOcspParityBatch2Tests.cs index 7832216..5f9b819 100644 --- a/tests/NATS.Server.Tests/TlsOcspParityBatch2Tests.cs +++ b/tests/NATS.Server.Transport.Tests/TlsOcspParityBatch2Tests.cs @@ -1,9 +1,10 @@ using System.Formats.Asn1; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using NATS.Server.TestUtilities; using NATS.Server.Tls; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class TlsOcspParityBatch2Tests { @@ -24,7 +25,7 @@ public class TlsOcspParityBatch2Tests [Fact] public void CertOCSPEligible_returns_false_when_leaf_has_no_ocsp_servers() { - var (leaf, _) = TlsHelperTests.GenerateTestCert(); + var (leaf, _) = TestCertHelper.GenerateTestCert(); var link = new ChainLink { Leaf = leaf }; TlsHelper.CertOCSPEligible(link).ShouldBeFalse(); diff --git a/tests/NATS.Server.Tests/TlsRateLimiterTests.cs b/tests/NATS.Server.Transport.Tests/TlsRateLimiterTests.cs similarity index 87% rename from tests/NATS.Server.Tests/TlsRateLimiterTests.cs rename to tests/NATS.Server.Transport.Tests/TlsRateLimiterTests.cs index 2e7e210..cac4382 100644 --- a/tests/NATS.Server.Tests/TlsRateLimiterTests.cs +++ b/tests/NATS.Server.Transport.Tests/TlsRateLimiterTests.cs @@ -1,6 +1,7 @@ +using NATS.Server.TestUtilities; using NATS.Server.Tls; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class TlsRateLimiterTests { @@ -39,8 +40,8 @@ public class TlsRateLimiterTests await limiter.WaitAsync(CancellationToken.None); await limiter.WaitAsync(CancellationToken.None); - // Wait for refill - await Task.Delay(1200); + // Wait for refill (rate limiter refills tokens after 1 second) + await PollHelper.YieldForAsync(1200); // Should have tokens again using var cts = new CancellationTokenSource(200); diff --git a/tests/NATS.Server.Tests/TlsServerTests.cs b/tests/NATS.Server.Transport.Tests/TlsServerTests.cs similarity index 82% rename from tests/NATS.Server.Tests/TlsServerTests.cs rename to tests/NATS.Server.Transport.Tests/TlsServerTests.cs index 703b1bf..ea7db4a 100644 --- a/tests/NATS.Server.Tests/TlsServerTests.cs +++ b/tests/NATS.Server.Transport.Tests/TlsServerTests.cs @@ -4,8 +4,9 @@ using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server; +using NATS.Server.TestUtilities; -namespace NATS.Server.Tests; +namespace NATS.Server.Transport.Tests; public class TlsServerTests : IAsyncLifetime { @@ -17,8 +18,8 @@ public class TlsServerTests : IAsyncLifetime public TlsServerTests() { - _port = GetFreePort(); - (_certPath, _keyPath) = TlsHelperTests.GenerateTestCertFiles(); + _port = TestPortAllocator.GetFreePort(); + (_certPath, _keyPath) = TestCertHelper.GenerateTestCertFiles(); _server = new NatsServer( new NatsOptions { @@ -98,25 +99,11 @@ public class TlsServerTests : IAsyncLifetime await ssl2.FlushAsync(); // Client 1 should receive MSG (may arrive across multiple TLS records) - var msg = await ReadUntilAsync(ssl1, "hello"); + var msg = await SocketTestHelper.ReadUntilAsync(ssl1, "hello"); msg.ShouldContain("MSG test 1 5"); msg.ShouldContain("hello"); } - private static async Task ReadUntilAsync(Stream stream, string expected, int timeoutMs = 5000) - { - using var cts = new CancellationTokenSource(timeoutMs); - var sb = new StringBuilder(); - var buf = new byte[4096]; - while (!sb.ToString().Contains(expected)) - { - var n = await stream.ReadAsync(buf, cts.Token); - if (n == 0) break; - sb.Append(Encoding.ASCII.GetString(buf, 0, n)); - } - return sb.ToString(); - } - private static async Task UpgradeToTlsAsync(TcpClient tcp) { var netStream = tcp.GetStream(); @@ -127,13 +114,6 @@ public class TlsServerTests : IAsyncLifetime await ssl.AuthenticateAsClientAsync("localhost"); return ssl; } - - private static int GetFreePort() - { - using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - sock.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - return ((IPEndPoint)sock.LocalEndPoint!).Port; - } } public class TlsMixedModeTests : IAsyncLifetime @@ -146,8 +126,8 @@ public class TlsMixedModeTests : IAsyncLifetime public TlsMixedModeTests() { - _port = GetFreePort(); - (_certPath, _keyPath) = TlsHelperTests.GenerateTestCertFiles(); + _port = TestPortAllocator.GetFreePort(); + (_certPath, _keyPath) = TestCertHelper.GenerateTestCertFiles(); _server = new NatsServer( new NatsOptions { @@ -215,11 +195,4 @@ public class TlsMixedModeTests : IAsyncLifetime var pong = Encoding.ASCII.GetString(pongBuf, 0, read); pong.ShouldContain("PONG"); } - - private static int GetFreePort() - { - using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - sock.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - return ((IPEndPoint)sock.LocalEndPoint!).Port; - } } diff --git a/tests/NATS.Server.Tests/WebSocket/WebSocketOptionsTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WebSocketOptionsTests.cs similarity index 96% rename from tests/NATS.Server.Tests/WebSocket/WebSocketOptionsTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WebSocketOptionsTests.cs index a4c9443..3836fba 100644 --- a/tests/NATS.Server.Tests/WebSocket/WebSocketOptionsTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WebSocketOptionsTests.cs @@ -1,7 +1,7 @@ using Shouldly; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WebSocketOptionsTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WebSocketOptionsValidatorParityBatch2Tests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WebSocketOptionsValidatorParityBatch2Tests.cs similarity index 99% rename from tests/NATS.Server.Tests/WebSocket/WebSocketOptionsValidatorParityBatch2Tests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WebSocketOptionsValidatorParityBatch2Tests.cs index e5706a4..87b4961 100644 --- a/tests/NATS.Server.Tests/WebSocket/WebSocketOptionsValidatorParityBatch2Tests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WebSocketOptionsValidatorParityBatch2Tests.cs @@ -1,7 +1,7 @@ using NATS.Server.Auth; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WebSocketOptionsValidatorParityBatch2Tests { diff --git a/tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WebSocketTlsTests.cs similarity index 98% rename from tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WebSocketTlsTests.cs index 2c99e54..8975eab 100644 --- a/tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WebSocketTlsTests.cs @@ -3,7 +3,7 @@ using NATS.Server.WebSocket; using Shouldly; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; /// /// Tests for — WebSocket-specific TLS configuration diff --git a/tests/NATS.Server.Tests/WebSocket/WsCompressionNegotiationTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsCompressionNegotiationTests.cs similarity index 99% rename from tests/NATS.Server.Tests/WebSocket/WsCompressionNegotiationTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsCompressionNegotiationTests.cs index efef0c4..58c87e2 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsCompressionNegotiationTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsCompressionNegotiationTests.cs @@ -6,7 +6,7 @@ using System.Text; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsCompressionNegotiationTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsCompressionTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsCompressionTests.cs similarity index 97% rename from tests/NATS.Server.Tests/WebSocket/WsCompressionTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsCompressionTests.cs index 425534c..ab59a62 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsCompressionTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsCompressionTests.cs @@ -1,7 +1,7 @@ using NATS.Server.WebSocket; using Shouldly; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsCompressionTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsConnectionTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsConnectionTests.cs similarity index 98% rename from tests/NATS.Server.Tests/WebSocket/WsConnectionTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsConnectionTests.cs index 8f30768..9f5e1f9 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsConnectionTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsConnectionTests.cs @@ -1,7 +1,7 @@ using System.Buffers.Binary; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsConnectionTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsConstantsTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsConstantsTests.cs similarity index 96% rename from tests/NATS.Server.Tests/WebSocket/WsConstantsTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsConstantsTests.cs index 3dd0b33..3e6d123 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsConstantsTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsConstantsTests.cs @@ -1,7 +1,7 @@ using NATS.Server.WebSocket; using Shouldly; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsConstantsTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsFrameReadTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsFrameReadTests.cs similarity index 99% rename from tests/NATS.Server.Tests/WebSocket/WsFrameReadTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsFrameReadTests.cs index 7e0e9df..38e678e 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsFrameReadTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsFrameReadTests.cs @@ -2,7 +2,7 @@ using System.Buffers.Binary; using NATS.Server.WebSocket; using Shouldly; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsFrameReadTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsFrameWriterTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsFrameWriterTests.cs similarity index 99% rename from tests/NATS.Server.Tests/WebSocket/WsFrameWriterTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsFrameWriterTests.cs index 153b120..41b2ee2 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsFrameWriterTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsFrameWriterTests.cs @@ -2,7 +2,7 @@ using System.Buffers.Binary; using NATS.Server.WebSocket; using Shouldly; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsFrameWriterTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsGoParityTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsGoParityTests.cs similarity index 99% rename from tests/NATS.Server.Tests/WebSocket/WsGoParityTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsGoParityTests.cs index efd6090..a5ad655 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsGoParityTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsGoParityTests.cs @@ -8,7 +8,7 @@ using System.Buffers.Binary; using System.Text; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; /// /// Parity tests ported from Go server/websocket_test.go exercising WebSocket diff --git a/tests/NATS.Server.Tests/WebSocket/WsIntegrationTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsIntegrationTests.cs similarity index 93% rename from tests/NATS.Server.Tests/WebSocket/WsIntegrationTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsIntegrationTests.cs index c20edee..e2faa3a 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsIntegrationTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsIntegrationTests.cs @@ -5,7 +5,7 @@ using System.Security.Cryptography; using System.Text; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsIntegrationTests : IAsyncLifetime { @@ -67,8 +67,12 @@ public class WsIntegrationTests : IAsyncLifetime using var sub = await ConnectWsClient(); using var pub = await ConnectWsClient(); - await SendWsText(sub, "CONNECT {}\r\nSUB test.ws 1\r\n"); - await Task.Delay(200); + await SendWsText(sub, "CONNECT {}\r\nSUB test.ws 1\r\nPING\r\n"); + + // Wait for PONG to confirm subscription is registered + using var subCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var pong = await ReadWsFrameAsync(sub, subCts.Token); + Encoding.ASCII.GetString(pong).ShouldContain("PONG"); await SendWsText(pub, "CONNECT {}\r\nPUB test.ws 5\r\nHello\r\n"); diff --git a/tests/NATS.Server.Tests/WebSocket/WsJwtAuthTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsJwtAuthTests.cs similarity index 99% rename from tests/NATS.Server.Tests/WebSocket/WsJwtAuthTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsJwtAuthTests.cs index 7f90df2..3269eda 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsJwtAuthTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsJwtAuthTests.cs @@ -6,7 +6,7 @@ using System.Text; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsJwtAuthTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsOriginCheckerTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsOriginCheckerTests.cs similarity index 98% rename from tests/NATS.Server.Tests/WebSocket/WsOriginCheckerTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsOriginCheckerTests.cs index ebd3531..4557a73 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsOriginCheckerTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsOriginCheckerTests.cs @@ -1,7 +1,7 @@ using NATS.Server.WebSocket; using Shouldly; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsOriginCheckerTests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsUpgradeHelperParityBatch1Tests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsUpgradeHelperParityBatch1Tests.cs similarity index 97% rename from tests/NATS.Server.Tests/WebSocket/WsUpgradeHelperParityBatch1Tests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsUpgradeHelperParityBatch1Tests.cs index 45004cf..948107f 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsUpgradeHelperParityBatch1Tests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsUpgradeHelperParityBatch1Tests.cs @@ -1,7 +1,7 @@ using System.Text; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsUpgradeHelperParityBatch1Tests { diff --git a/tests/NATS.Server.Tests/WebSocket/WsUpgradeTests.cs b/tests/NATS.Server.Transport.Tests/WebSocket/WsUpgradeTests.cs similarity index 99% rename from tests/NATS.Server.Tests/WebSocket/WsUpgradeTests.cs rename to tests/NATS.Server.Transport.Tests/WebSocket/WsUpgradeTests.cs index a5e1168..a92c916 100644 --- a/tests/NATS.Server.Tests/WebSocket/WsUpgradeTests.cs +++ b/tests/NATS.Server.Transport.Tests/WebSocket/WsUpgradeTests.cs @@ -1,7 +1,7 @@ using System.Text; using NATS.Server.WebSocket; -namespace NATS.Server.Tests.WebSocket; +namespace NATS.Server.Transport.Tests.WebSocket; public class WsUpgradeTests {