feat: execute full-repo remaining parity closure plan
This commit is contained in:
30
tests/NATS.Server.Tests/Auth/AuthExtensionParityTests.cs
Normal file
30
tests/NATS.Server.Tests/Auth/AuthExtensionParityTests.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class AuthExtensionParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Auth_service_uses_proxy_auth_extension_when_enabled()
|
||||
{
|
||||
var service = AuthService.Build(new NatsOptions
|
||||
{
|
||||
ProxyAuth = new ProxyAuthOptions
|
||||
{
|
||||
Enabled = true,
|
||||
UsernamePrefix = "proxy:",
|
||||
},
|
||||
});
|
||||
|
||||
service.IsAuthRequired.ShouldBeTrue();
|
||||
var result = service.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "proxy:alice" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe("alice");
|
||||
}
|
||||
}
|
||||
58
tests/NATS.Server.Tests/Auth/ExternalAuthCalloutTests.cs
Normal file
58
tests/NATS.Server.Tests/Auth/ExternalAuthCalloutTests.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ExternalAuthCalloutTests
|
||||
{
|
||||
[Fact]
|
||||
public void External_callout_authenticator_can_allow_and_deny_with_timeout_and_reason_mapping()
|
||||
{
|
||||
var authenticator = new ExternalAuthCalloutAuthenticator(
|
||||
new FakeExternalAuthClient(),
|
||||
TimeSpan.FromMilliseconds(50));
|
||||
|
||||
var allowed = authenticator.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "u", Password = "p" },
|
||||
Nonce = [],
|
||||
});
|
||||
allowed.ShouldNotBeNull();
|
||||
allowed.Identity.ShouldBe("u");
|
||||
|
||||
var denied = authenticator.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "u", Password = "bad" },
|
||||
Nonce = [],
|
||||
});
|
||||
denied.ShouldBeNull();
|
||||
|
||||
var timeout = new ExternalAuthCalloutAuthenticator(
|
||||
new SlowExternalAuthClient(TimeSpan.FromMilliseconds(200)),
|
||||
TimeSpan.FromMilliseconds(30));
|
||||
timeout.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "u", Password = "p" },
|
||||
Nonce = [],
|
||||
}).ShouldBeNull();
|
||||
}
|
||||
|
||||
private sealed class FakeExternalAuthClient : IExternalAuthClient
|
||||
{
|
||||
public Task<ExternalAuthDecision> AuthorizeAsync(ExternalAuthRequest request, CancellationToken ct)
|
||||
{
|
||||
if (request is { Username: "u", Password: "p" })
|
||||
return Task.FromResult(new ExternalAuthDecision(true, "u", "A"));
|
||||
return Task.FromResult(new ExternalAuthDecision(false, Reason: "denied"));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SlowExternalAuthClient(TimeSpan delay) : IExternalAuthClient
|
||||
{
|
||||
public async Task<ExternalAuthDecision> AuthorizeAsync(ExternalAuthRequest request, CancellationToken ct)
|
||||
{
|
||||
await Task.Delay(delay, ct);
|
||||
return new ExternalAuthDecision(true, "slow");
|
||||
}
|
||||
}
|
||||
}
|
||||
28
tests/NATS.Server.Tests/Auth/ProxyAuthTests.cs
Normal file
28
tests/NATS.Server.Tests/Auth/ProxyAuthTests.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ProxyAuthTests
|
||||
{
|
||||
[Fact]
|
||||
public void Proxy_authenticator_maps_prefixed_username_to_identity()
|
||||
{
|
||||
var authenticator = new ProxyAuthenticator(new ProxyAuthOptions
|
||||
{
|
||||
Enabled = true,
|
||||
UsernamePrefix = "proxy:",
|
||||
Account = "A",
|
||||
});
|
||||
|
||||
var result = authenticator.Authenticate(new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { Username = "proxy:bob" },
|
||||
Nonce = [],
|
||||
});
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe("bob");
|
||||
result.AccountName.ShouldBe("A");
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,11 @@ namespace NATS.Server.Tests;
|
||||
public class DifferencesParityClosureTests
|
||||
{
|
||||
[Fact]
|
||||
public void Differences_md_has_no_remaining_jetstream_baseline_or_n_rows()
|
||||
public void Differences_md_has_no_remaining_baseline_n_or_stub_rows_in_tracked_scope()
|
||||
{
|
||||
var repositoryRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
var differencesPath = Path.Combine(repositoryRoot, "differences.md");
|
||||
File.Exists(differencesPath).ShouldBeTrue();
|
||||
|
||||
var markdown = File.ReadAllText(differencesPath);
|
||||
markdown.ShouldContain("### JetStream");
|
||||
markdown.ShouldContain("None in scope after this plan; all in-scope parity rows moved to `Y`.");
|
||||
var report = Parity.ParityRowInspector.Load("differences.md");
|
||||
report.UnresolvedRows.ShouldBeEmpty(string.Join(
|
||||
Environment.NewLine,
|
||||
report.UnresolvedRows.Select(r => $"{r.Section} :: {r.SubSection} :: {r.Feature} [{r.DotNetStatus}]")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using NATS.Server.Gateways;
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class GatewayInterestOnlyParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Gateway_interest_only_mode_forwards_only_subjects_with_remote_interest()
|
||||
{
|
||||
using var subList = new SubList();
|
||||
subList.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "gw1", "A"));
|
||||
|
||||
GatewayManager.ShouldForwardInterestOnly(subList, "A", "orders.created").ShouldBeTrue();
|
||||
GatewayManager.ShouldForwardInterestOnly(subList, "A", "payments.created").ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
17
tests/NATS.Server.Tests/IO/AdaptiveReadBufferTests.cs
Normal file
17
tests/NATS.Server.Tests/IO/AdaptiveReadBufferTests.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NATS.Server.IO;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class AdaptiveReadBufferTests
|
||||
{
|
||||
[Fact]
|
||||
public void Read_buffer_scales_between_512_and_65536_based_on_recent_payload_pattern()
|
||||
{
|
||||
var b = new AdaptiveReadBuffer();
|
||||
b.RecordRead(512);
|
||||
b.RecordRead(4096);
|
||||
b.RecordRead(32000);
|
||||
b.CurrentSize.ShouldBeGreaterThan(4096);
|
||||
b.CurrentSize.ShouldBeLessThanOrEqualTo(64 * 1024);
|
||||
}
|
||||
}
|
||||
17
tests/NATS.Server.Tests/IO/OutboundBufferPoolTests.cs
Normal file
17
tests/NATS.Server.Tests/IO/OutboundBufferPoolTests.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NATS.Server.IO;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class OutboundBufferPoolTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(100, 512)]
|
||||
[InlineData(1000, 4096)]
|
||||
[InlineData(10000, 64 * 1024)]
|
||||
public void Rent_uses_three_tier_buffer_buckets(int requested, int expectedMinimum)
|
||||
{
|
||||
var pool = new OutboundBufferPool();
|
||||
using var owner = pool.Rent(requested);
|
||||
owner.Memory.Length.ShouldBeGreaterThanOrEqualTo(expectedMinimum);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using NATS.Server.JetStream.Cluster;
|
||||
using NATS.Server.JetStream.Models;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamClusterGovernanceRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Jetstream_cluster_governance_applies_consensus_backed_placement()
|
||||
{
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
await meta.ProposeCreateStreamAsync(new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Subjects = ["orders.*"],
|
||||
}, default);
|
||||
|
||||
var planner = new AssetPlacementPlanner(3);
|
||||
var placement = planner.PlanReplicas(2);
|
||||
var replicas = new StreamReplicaGroup("ORDERS", 1);
|
||||
await replicas.ApplyPlacementAsync(placement, default);
|
||||
|
||||
meta.GetState().Streams.ShouldContain("ORDERS");
|
||||
replicas.Nodes.Count.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using NATS.Server.JetStream.Consumers;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.JetStream.Storage;
|
||||
using NATS.Server.JetStream;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamConsumerFlowReplayParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Push_consumer_enqueues_flow_control_and_heartbeat_frames_when_enabled()
|
||||
{
|
||||
var engine = new PushConsumerEngine();
|
||||
var consumer = new ConsumerHandle("ORDERS", new ConsumerConfig
|
||||
{
|
||||
AckPolicy = AckPolicy.Explicit,
|
||||
FlowControl = true,
|
||||
HeartbeatMs = 1000,
|
||||
RateLimitBps = 1024,
|
||||
});
|
||||
|
||||
engine.Enqueue(consumer, new StoredMessage
|
||||
{
|
||||
Sequence = 1,
|
||||
Subject = "orders.created",
|
||||
Payload = "payload"u8.ToArray(),
|
||||
});
|
||||
|
||||
consumer.PushFrames.Count.ShouldBe(3);
|
||||
consumer.PushFrames.Any(f => f.IsFlowControl).ShouldBeTrue();
|
||||
consumer.PushFrames.Any(f => f.IsHeartbeat).ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using NATS.Server.JetStream.Consumers;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamConsumerRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Consumer_runtime_honors_ack_all_redelivery_and_max_deliver_limits()
|
||||
{
|
||||
var ack = new AckProcessor();
|
||||
ack.Register(1, ackWaitMs: 1);
|
||||
await Task.Delay(5);
|
||||
ack.TryGetExpired(out var seq, out var deliveries).ShouldBeTrue();
|
||||
seq.ShouldBe((ulong)1);
|
||||
deliveries.ShouldBe(1);
|
||||
|
||||
ack.ScheduleRedelivery(seq, delayMs: 1);
|
||||
await Task.Delay(5);
|
||||
ack.TryGetExpired(out _, out deliveries).ShouldBeTrue();
|
||||
deliveries.ShouldBe(2);
|
||||
|
||||
ack.AckAll(1);
|
||||
ack.HasPending.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Configuration;
|
||||
using NATS.Server.Gateways;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamCrossClusterRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Jetstream_cross_cluster_messages_are_forward_counted()
|
||||
{
|
||||
var manager = new GatewayManager(
|
||||
new GatewayOptions { Host = "127.0.0.1", Port = 0, Name = "A" },
|
||||
new ServerStats(),
|
||||
"S1",
|
||||
_ => { },
|
||||
_ => { },
|
||||
NullLogger<GatewayManager>.Instance);
|
||||
|
||||
await manager.ForwardJetStreamClusterMessageAsync(
|
||||
new GatewayMessage("$JS.CLUSTER.REPL", null, "x"u8.ToArray()),
|
||||
default);
|
||||
|
||||
manager.ForwardedJetStreamClusterMessages.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using NATS.Server.JetStream.Storage;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamFileStoreCryptoCompressionTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task File_store_compression_and_encryption_roundtrip_preserves_payload()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"natsdotnet-filestore-crypto-{Guid.NewGuid():N}");
|
||||
try
|
||||
{
|
||||
await using var store = new FileStore(new FileStoreOptions
|
||||
{
|
||||
Directory = dir,
|
||||
EnableCompression = true,
|
||||
EnableEncryption = true,
|
||||
EncryptionKey = [1, 2, 3, 4],
|
||||
});
|
||||
|
||||
var payload = Enumerable.Repeat((byte)'a', 512).ToArray();
|
||||
var seq = await store.AppendAsync("orders.created", payload, default);
|
||||
var loaded = await store.LoadAsync(seq, default);
|
||||
loaded.ShouldNotBeNull();
|
||||
loaded.Payload.ToArray().ShouldBe(payload);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using NATS.Server.JetStream.Storage;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamFileStoreLayoutParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task File_store_uses_block_index_layout_with_ttl_prune_invariants()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"natsdotnet-filestore-{Guid.NewGuid():N}");
|
||||
try
|
||||
{
|
||||
await using var store = new FileStore(new FileStoreOptions
|
||||
{
|
||||
Directory = dir,
|
||||
BlockSizeBytes = 128,
|
||||
MaxAgeMs = 60_000,
|
||||
});
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
await store.AppendAsync($"orders.{i}", "x"u8.ToArray(), default);
|
||||
|
||||
var state = await store.GetStateAsync(default);
|
||||
state.Messages.ShouldBe((ulong)100);
|
||||
store.BlockCount.ShouldBeGreaterThan(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using NATS.Server.JetStream.MirrorSource;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.JetStream.Storage;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamMirrorSourceRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Mirror_source_runtime_tracks_sync_state_and_subject_mapping()
|
||||
{
|
||||
var mirrorTarget = new MemStore();
|
||||
var sourceTarget = new MemStore();
|
||||
var mirror = new MirrorCoordinator(mirrorTarget);
|
||||
var source = new SourceCoordinator(sourceTarget, new StreamSourceConfig
|
||||
{
|
||||
Name = "SRC",
|
||||
SubjectTransformPrefix = "agg.",
|
||||
SourceAccount = "A",
|
||||
});
|
||||
|
||||
var message = new StoredMessage
|
||||
{
|
||||
Sequence = 10,
|
||||
Subject = "orders.created",
|
||||
Payload = "ok"u8.ToArray(),
|
||||
TimestampUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await mirror.OnOriginAppendAsync(message, default);
|
||||
await source.OnOriginAppendAsync(message, default);
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe((ulong)10);
|
||||
source.LastOriginSequence.ShouldBe((ulong)10);
|
||||
|
||||
var sourced = await sourceTarget.LoadAsync(1, default);
|
||||
sourced.ShouldNotBeNull();
|
||||
sourced.Subject.ShouldBe("agg.orders.created");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.JetStream.Validation;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamStreamFeatureToggleParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Stream_feature_toggles_are_preserved_in_config_model_and_validation()
|
||||
{
|
||||
var config = new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Subjects = ["orders.*"],
|
||||
Sealed = true,
|
||||
DenyDelete = true,
|
||||
DenyPurge = true,
|
||||
AllowDirect = true,
|
||||
MaxMsgSize = 1024,
|
||||
MaxMsgsPer = 10,
|
||||
MaxAgeMs = 5000,
|
||||
};
|
||||
|
||||
JetStreamConfigValidator.Validate(config).IsValid.ShouldBeTrue();
|
||||
config.Sealed.ShouldBeTrue();
|
||||
config.DenyDelete.ShouldBeTrue();
|
||||
config.DenyPurge.ShouldBeTrue();
|
||||
config.AllowDirect.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.JetStream.Publish;
|
||||
using NATS.Server.JetStream.Validation;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamStreamRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Stream_runtime_enforces_retention_and_size_preconditions()
|
||||
{
|
||||
var invalid = new StreamConfig
|
||||
{
|
||||
Name = "ORDERS",
|
||||
Subjects = ["orders.*"],
|
||||
Retention = RetentionPolicy.WorkQueue,
|
||||
MaxConsumers = 0,
|
||||
MaxMsgSize = -1,
|
||||
};
|
||||
|
||||
var result = JetStreamConfigValidator.Validate(invalid);
|
||||
result.IsValid.ShouldBeFalse();
|
||||
|
||||
var preconditions = new PublishPreconditions();
|
||||
preconditions.Record("m1", 1);
|
||||
preconditions.IsDuplicate("m1", duplicateWindowMs: 10_000, out var existing).ShouldBeTrue();
|
||||
existing.ShouldBe((ulong)1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using NATS.Server.LeafNodes;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class LeafHubSpokeMappingParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Leaf_hub_spoke_mapper_round_trips_account_mapping()
|
||||
{
|
||||
var mapper = new LeafHubSpokeMapper(new Dictionary<string, string>
|
||||
{
|
||||
["HUB"] = "SPOKE",
|
||||
});
|
||||
|
||||
var outbound = mapper.Map("HUB", "orders.created", LeafMapDirection.Outbound);
|
||||
outbound.Account.ShouldBe("SPOKE");
|
||||
|
||||
var inbound = mapper.Map("SPOKE", "orders.created", LeafMapDirection.Inbound);
|
||||
inbound.Account.ShouldBe("HUB");
|
||||
}
|
||||
}
|
||||
21
tests/NATS.Server.Tests/Monitoring/ConnzParityFieldTests.cs
Normal file
21
tests/NATS.Server.Tests/Monitoring/ConnzParityFieldTests.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ConnzParityFieldTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Connz_includes_identity_tls_and_proxy_parity_fields()
|
||||
{
|
||||
await using var fx = await MonitoringParityFixture.StartAsync();
|
||||
await fx.ConnectClientAsync("u", "orders.created");
|
||||
|
||||
var connz = fx.GetConnz("?subs=detail");
|
||||
connz.Conns.ShouldNotBeEmpty();
|
||||
|
||||
var json = JsonSerializer.Serialize(connz);
|
||||
json.ShouldContain("tls_peer_cert_subject");
|
||||
json.ShouldContain("jwt_issuer_key");
|
||||
json.ShouldContain("proxy");
|
||||
}
|
||||
}
|
||||
115
tests/NATS.Server.Tests/Monitoring/ConnzParityFilterTests.cs
Normal file
115
tests/NATS.Server.Tests/Monitoring/ConnzParityFilterTests.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Monitoring;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ConnzParityFilterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Connz_filters_by_user_account_and_subject_and_includes_tls_peer_and_jwt_metadata()
|
||||
{
|
||||
await using var fx = await MonitoringParityFixture.StartAsync();
|
||||
await fx.ConnectClientAsync("u", "orders.created");
|
||||
await fx.ConnectClientAsync("v", "payments.created");
|
||||
|
||||
var connz = fx.GetConnz("?user=u&acc=A&filter_subject=orders.*&subs=detail");
|
||||
connz.Conns.ShouldAllBe(c => c.Account == "A" && c.AuthorizedUser == "u");
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MonitoringParityFixture : IAsyncDisposable
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly List<TcpClient> _clients = [];
|
||||
private readonly NatsOptions _options;
|
||||
|
||||
private MonitoringParityFixture(NatsServer server, NatsOptions options, CancellationTokenSource cts)
|
||||
{
|
||||
_server = server;
|
||||
_options = options;
|
||||
_cts = cts;
|
||||
}
|
||||
|
||||
public static async Task<MonitoringParityFixture> StartAsync()
|
||||
{
|
||||
var options = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
Users =
|
||||
[
|
||||
new User { Username = "u", Password = "p", Account = "A" },
|
||||
new User { Username = "v", Password = "p", Account = "B" },
|
||||
],
|
||||
};
|
||||
|
||||
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
return new MonitoringParityFixture(server, options, cts);
|
||||
}
|
||||
|
||||
public async Task ConnectClientAsync(string username, string? subscribeSubject)
|
||||
{
|
||||
var client = new TcpClient();
|
||||
await client.ConnectAsync(IPAddress.Loopback, _server.Port);
|
||||
_clients.Add(client);
|
||||
|
||||
var stream = client.GetStream();
|
||||
await ReadLineAsync(stream); // INFO
|
||||
|
||||
var connect = $"CONNECT {{\"user\":\"{username}\",\"pass\":\"p\"}}\r\n";
|
||||
await stream.WriteAsync(Encoding.ASCII.GetBytes(connect));
|
||||
if (!string.IsNullOrEmpty(subscribeSubject))
|
||||
await stream.WriteAsync(Encoding.ASCII.GetBytes($"SUB {subscribeSubject} sid-{username}\r\n"));
|
||||
await stream.FlushAsync();
|
||||
await Task.Delay(30);
|
||||
}
|
||||
|
||||
public Connz GetConnz(string queryString)
|
||||
{
|
||||
var ctx = new DefaultHttpContext();
|
||||
ctx.Request.QueryString = new QueryString(queryString);
|
||||
return new ConnzHandler(_server).HandleConnz(ctx);
|
||||
}
|
||||
|
||||
public async Task<Varz> GetVarzAsync()
|
||||
{
|
||||
using var handler = new VarzHandler(_server, _options);
|
||||
return await handler.HandleVarzAsync();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var client in _clients)
|
||||
client.Dispose();
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
private static async Task<string> ReadLineAsync(NetworkStream stream)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
var one = new byte[1];
|
||||
while (true)
|
||||
{
|
||||
var read = await stream.ReadAsync(one.AsMemory(0, 1));
|
||||
if (read == 0)
|
||||
break;
|
||||
if (one[0] == (byte)'\n')
|
||||
break;
|
||||
if (one[0] != (byte)'\r')
|
||||
bytes.Add(one[0]);
|
||||
}
|
||||
|
||||
return Encoding.ASCII.GetString([.. bytes]);
|
||||
}
|
||||
}
|
||||
87
tests/NATS.Server.Tests/Monitoring/PprofEndpointTests.cs
Normal file
87
tests/NATS.Server.Tests/Monitoring/PprofEndpointTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class PprofEndpointTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Debug_pprof_endpoint_returns_profile_index_when_profport_enabled()
|
||||
{
|
||||
await using var fx = await PprofMonitorFixture.StartWithProfilingAsync();
|
||||
var body = await fx.GetStringAsync("/debug/pprof");
|
||||
body.ShouldContain("profiles");
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PprofMonitorFixture : IAsyncDisposable
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly HttpClient _http;
|
||||
private readonly int _monitorPort;
|
||||
|
||||
private PprofMonitorFixture(NatsServer server, CancellationTokenSource cts, HttpClient http, int monitorPort)
|
||||
{
|
||||
_server = server;
|
||||
_cts = cts;
|
||||
_http = http;
|
||||
_monitorPort = monitorPort;
|
||||
}
|
||||
|
||||
public static async Task<PprofMonitorFixture> StartWithProfilingAsync()
|
||||
{
|
||||
var monitorPort = GetFreePort();
|
||||
var options = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
MonitorPort = monitorPort,
|
||||
ProfPort = monitorPort,
|
||||
};
|
||||
|
||||
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
var http = new HttpClient();
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await http.GetAsync($"http://127.0.0.1:{monitorPort}/healthz");
|
||||
if (response.IsSuccessStatusCode)
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
return new PprofMonitorFixture(server, cts, http, monitorPort);
|
||||
}
|
||||
|
||||
public Task<string> GetStringAsync(string path)
|
||||
{
|
||||
return _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}{path}");
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_http.Dispose();
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)socket.LocalEndPoint!).Port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class VarzSlowConsumerBreakdownTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Varz_contains_slow_consumer_breakdown_fields()
|
||||
{
|
||||
await using var fx = await MonitoringParityFixture.StartAsync();
|
||||
var varz = await fx.GetVarzAsync();
|
||||
|
||||
varz.SlowConsumerStats.ShouldNotBeNull();
|
||||
varz.SlowConsumerStats.Clients.ShouldBeGreaterThanOrEqualTo((ulong)0);
|
||||
varz.SlowConsumerStats.Routes.ShouldBeGreaterThanOrEqualTo((ulong)0);
|
||||
varz.SlowConsumerStats.Gateways.ShouldBeGreaterThanOrEqualTo((ulong)0);
|
||||
varz.SlowConsumerStats.Leafs.ShouldBeGreaterThanOrEqualTo((ulong)0);
|
||||
}
|
||||
}
|
||||
73
tests/NATS.Server.Tests/Mqtt/MqttListenerParityTests.cs
Normal file
73
tests/NATS.Server.Tests/Mqtt/MqttListenerParityTests.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using NATS.Server.Mqtt;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class MqttListenerParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Mqtt_listener_accepts_connect_and_routes_publish_to_matching_subscription()
|
||||
{
|
||||
await using var listener = new MqttListener("127.0.0.1", 0);
|
||||
using var cts = new CancellationTokenSource();
|
||||
await listener.StartAsync(cts.Token);
|
||||
|
||||
using var sub = new TcpClient();
|
||||
await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
|
||||
var subStream = sub.GetStream();
|
||||
await MqttTestWire.WriteLineAsync(subStream, "CONNECT sub");
|
||||
(await MqttTestWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK");
|
||||
await MqttTestWire.WriteLineAsync(subStream, "SUB sensors.temp");
|
||||
var subAck = await MqttTestWire.ReadLineAsync(subStream, 1000);
|
||||
subAck.ShouldNotBeNull();
|
||||
subAck.ShouldContain("SUBACK");
|
||||
|
||||
using var pub = new TcpClient();
|
||||
await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
|
||||
var pubStream = pub.GetStream();
|
||||
await MqttTestWire.WriteLineAsync(pubStream, "CONNECT pub");
|
||||
_ = await MqttTestWire.ReadLineAsync(pubStream, 1000);
|
||||
await MqttTestWire.WriteLineAsync(pubStream, "PUB sensors.temp 42");
|
||||
|
||||
var message = await MqttTestWire.ReadLineAsync(subStream, 1000);
|
||||
message.ShouldBe("MSG sensors.temp 42");
|
||||
}
|
||||
}
|
||||
|
||||
internal static class MqttTestWire
|
||||
{
|
||||
public static async Task WriteLineAsync(NetworkStream stream, string line)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(line + "\n");
|
||||
await stream.WriteAsync(bytes);
|
||||
await stream.FlushAsync();
|
||||
}
|
||||
|
||||
public static async Task<string?> ReadLineAsync(NetworkStream stream, int timeoutMs)
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(timeoutMs);
|
||||
var bytes = new List<byte>();
|
||||
var one = new byte[1];
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token);
|
||||
if (read == 0)
|
||||
return null;
|
||||
if (one[0] == (byte)'\n')
|
||||
break;
|
||||
if (one[0] != (byte)'\r')
|
||||
bytes.Add(one[0]);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString([.. bytes]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using NATS.Server.Mqtt;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class MqttPublishSubscribeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Mqtt_publish_only_reaches_matching_topic_subscribers()
|
||||
{
|
||||
await using var listener = new MqttListener("127.0.0.1", 0);
|
||||
using var cts = new CancellationTokenSource();
|
||||
await listener.StartAsync(cts.Token);
|
||||
|
||||
using var sub = new TcpClient();
|
||||
await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
|
||||
var subStream = sub.GetStream();
|
||||
await MqttTestWire.WriteLineAsync(subStream, "CONNECT sub");
|
||||
_ = await MqttTestWire.ReadLineAsync(subStream, 1000);
|
||||
await MqttTestWire.WriteLineAsync(subStream, "SUB sensors.temp");
|
||||
_ = await MqttTestWire.ReadLineAsync(subStream, 1000);
|
||||
|
||||
using var pub = new TcpClient();
|
||||
await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
|
||||
var pubStream = pub.GetStream();
|
||||
await MqttTestWire.WriteLineAsync(pubStream, "CONNECT pub");
|
||||
_ = await MqttTestWire.ReadLineAsync(pubStream, 1000);
|
||||
await MqttTestWire.WriteLineAsync(pubStream, "PUB sensors.humidity 90");
|
||||
|
||||
(await MqttTestWire.ReadLineAsync(subStream, 150)).ShouldBeNull();
|
||||
}
|
||||
}
|
||||
67
tests/NATS.Server.Tests/Parity/ParityRowInspector.cs
Normal file
67
tests/NATS.Server.Tests/Parity/ParityRowInspector.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
namespace NATS.Server.Tests.Parity;
|
||||
|
||||
public sealed record ParityRow(string Section, string SubSection, string Feature, string DotNetStatus);
|
||||
|
||||
public sealed class ParityReport
|
||||
{
|
||||
public ParityReport(IReadOnlyList<ParityRow> rows)
|
||||
{
|
||||
Rows = rows;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ParityRow> Rows { get; }
|
||||
|
||||
public IReadOnlyList<ParityRow> UnresolvedRows =>
|
||||
Rows.Where(r => r.DotNetStatus is "N" or "Baseline" or "Stub").ToArray();
|
||||
}
|
||||
|
||||
public static class ParityRowInspector
|
||||
{
|
||||
public static ParityReport Load(string relativePath)
|
||||
{
|
||||
var repositoryRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
var differencesPath = Path.Combine(repositoryRoot, relativePath);
|
||||
File.Exists(differencesPath).ShouldBeTrue();
|
||||
|
||||
var section = string.Empty;
|
||||
var subsection = string.Empty;
|
||||
var rows = new List<ParityRow>();
|
||||
foreach (var rawLine in File.ReadLines(differencesPath))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.StartsWith("## ", StringComparison.Ordinal))
|
||||
{
|
||||
section = line[3..].Trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("### ", StringComparison.Ordinal))
|
||||
{
|
||||
subsection = line[4..].Trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.StartsWith("|", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
if (line.Contains("---", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var cells = line.Trim('|').Split('|').Select(c => c.Trim()).ToArray();
|
||||
if (cells.Length < 3)
|
||||
continue;
|
||||
|
||||
// Ignore table header rows; row format is expected to contain Go and .NET status columns.
|
||||
if (cells[0] is "Feature" or "Aspect" or "Operation" or "Signal" or "Type" or "Mechanism" or "Flag")
|
||||
continue;
|
||||
|
||||
rows.Add(new ParityRow(
|
||||
section,
|
||||
subsection,
|
||||
cells[0],
|
||||
cells[2]));
|
||||
}
|
||||
|
||||
return new ParityReport(rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class InterServerOpcodeRoutingTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parser_dispatch_rejects_Aplus_for_client_kind_client_but_allows_for_gateway()
|
||||
{
|
||||
var m = new ClientCommandMatrix();
|
||||
m.IsAllowed(ClientKind.Client, "A+").ShouldBeFalse();
|
||||
m.IsAllowed(ClientKind.Gateway, "A+").ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class MessageTraceInitializationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Trace_context_is_initialized_from_connect_options()
|
||||
{
|
||||
var connectOpts = new ClientOptions
|
||||
{
|
||||
Name = "c1",
|
||||
Lang = "dotnet",
|
||||
Version = "1.0.0",
|
||||
Headers = true,
|
||||
};
|
||||
|
||||
var ctx = MessageTraceContext.CreateFromConnect(connectOpts);
|
||||
ctx.ClientName.ShouldBe("c1");
|
||||
ctx.ClientLang.ShouldBe("dotnet");
|
||||
ctx.ClientVersion.ShouldBe("1.0.0");
|
||||
ctx.HeadersEnabled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using NATS.Server.Raft;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class RaftConsensusRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Raft_cluster_commits_with_next_index_backtracking_semantics()
|
||||
{
|
||||
var cluster = RaftTestCluster.Create(3);
|
||||
await cluster.GenerateCommittedEntriesAsync(5);
|
||||
await cluster.WaitForAppliedAsync(5);
|
||||
|
||||
cluster.Nodes.All(n => n.AppliedIndex >= 5).ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using NATS.Server.Raft;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class RaftMembershipRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Raft_membership_add_remove_round_trips()
|
||||
{
|
||||
var node = new RaftNode("N1");
|
||||
node.AddMember("N2");
|
||||
node.AddMember("N3");
|
||||
node.Members.ShouldContain("N2");
|
||||
node.Members.ShouldContain("N3");
|
||||
|
||||
node.RemoveMember("N2");
|
||||
node.Members.ShouldNotContain("N2");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class RaftSnapshotTransferRuntimeParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Raft_snapshot_install_catches_up_lagging_follower()
|
||||
{
|
||||
var cluster = RaftTestCluster.Create(3);
|
||||
await cluster.GenerateCommittedEntriesAsync(3);
|
||||
await cluster.RestartLaggingFollowerAsync();
|
||||
await cluster.WaitForFollowerCatchupAsync();
|
||||
|
||||
cluster.LaggingFollower.AppliedIndex.ShouldBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
14
tests/NATS.Server.Tests/Routes/RouteAccountScopedTests.cs
Normal file
14
tests/NATS.Server.Tests/Routes/RouteAccountScopedTests.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using NATS.Server.Routes;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class RouteAccountScopedTests
|
||||
{
|
||||
[Fact]
|
||||
public void Route_connect_info_includes_account_scope()
|
||||
{
|
||||
var json = RouteConnection.BuildConnectInfoJson("S1", ["A"], "topology-v1");
|
||||
json.ShouldContain("\"accounts\":[\"A\"]");
|
||||
json.ShouldContain("\"topology\":\"topology-v1\"");
|
||||
}
|
||||
}
|
||||
16
tests/NATS.Server.Tests/Routes/RouteCompressionTests.cs
Normal file
16
tests/NATS.Server.Tests/Routes/RouteCompressionTests.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Text;
|
||||
using NATS.Server.Routes;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class RouteCompressionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Route_payload_round_trips_through_compression_codec()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes(new string('x', 512));
|
||||
var compressed = RouteCompressionCodec.Compress(payload);
|
||||
var restored = RouteCompressionCodec.Decompress(compressed);
|
||||
restored.ShouldBe(payload);
|
||||
}
|
||||
}
|
||||
25
tests/NATS.Server.Tests/Routes/RouteTopologyGossipTests.cs
Normal file
25
tests/NATS.Server.Tests/Routes/RouteTopologyGossipTests.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Configuration;
|
||||
using NATS.Server.Routes;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class RouteTopologyGossipTests
|
||||
{
|
||||
[Fact]
|
||||
public void Topology_snapshot_reports_server_and_route_counts()
|
||||
{
|
||||
var manager = new RouteManager(
|
||||
new ClusterOptions { Host = "127.0.0.1", Port = 0 },
|
||||
new ServerStats(),
|
||||
"S1",
|
||||
_ => { },
|
||||
_ => { },
|
||||
NullLogger<RouteManager>.Instance);
|
||||
|
||||
var snapshot = manager.BuildTopologySnapshot();
|
||||
snapshot.ServerId.ShouldBe("S1");
|
||||
snapshot.RouteCount.ShouldBe(0);
|
||||
snapshot.ConnectedServerIds.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Server;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class AcceptLoopErrorCallbackTests
|
||||
{
|
||||
[Fact]
|
||||
public void Accept_loop_reports_error_via_callback_hook()
|
||||
{
|
||||
var server = new NatsServer(new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
}, NullLoggerFactory.Instance);
|
||||
|
||||
Exception? capturedError = null;
|
||||
EndPoint? capturedEndpoint = null;
|
||||
var capturedDelay = TimeSpan.Zero;
|
||||
|
||||
var handler = new AcceptLoopErrorHandler((ex, endpoint, delay) =>
|
||||
{
|
||||
capturedError = ex;
|
||||
capturedEndpoint = endpoint;
|
||||
capturedDelay = delay;
|
||||
});
|
||||
|
||||
server.SetAcceptLoopErrorHandlerForTest(handler);
|
||||
|
||||
var endpoint = new IPEndPoint(IPAddress.Loopback, 4222);
|
||||
var error = new SocketException((int)SocketError.ConnectionReset);
|
||||
var delay = TimeSpan.FromMilliseconds(20);
|
||||
server.NotifyAcceptErrorForTest(error, endpoint, delay);
|
||||
|
||||
capturedError.ShouldBe(error);
|
||||
capturedEndpoint.ShouldBe(endpoint);
|
||||
capturedDelay.ShouldBe(delay);
|
||||
}
|
||||
}
|
||||
84
tests/NATS.Server.Tests/Server/AcceptLoopReloadLockTests.cs
Normal file
84
tests/NATS.Server.Tests/Server/AcceptLoopReloadLockTests.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class AcceptLoopReloadLockTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Accept_loop_blocks_client_creation_while_reload_lock_is_held()
|
||||
{
|
||||
await using var fx = await AcceptLoopFixture.StartAsync();
|
||||
await fx.HoldReloadLockAsync();
|
||||
(await fx.TryConnectClientAsync(timeoutMs: 150)).ShouldBeFalse();
|
||||
fx.ReleaseReloadLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AcceptLoopFixture : IAsyncDisposable
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private bool _reloadHeld;
|
||||
|
||||
private AcceptLoopFixture(NatsServer server, CancellationTokenSource cts)
|
||||
{
|
||||
_server = server;
|
||||
_cts = cts;
|
||||
}
|
||||
|
||||
public static async Task<AcceptLoopFixture> StartAsync()
|
||||
{
|
||||
var server = new NatsServer(new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
}, NullLoggerFactory.Instance);
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
return new AcceptLoopFixture(server, cts);
|
||||
}
|
||||
|
||||
public async Task HoldReloadLockAsync()
|
||||
{
|
||||
await _server.AcquireReloadLockForTestAsync();
|
||||
_reloadHeld = true;
|
||||
}
|
||||
|
||||
public void ReleaseReloadLock()
|
||||
{
|
||||
if (_reloadHeld)
|
||||
{
|
||||
_server.ReleaseReloadLockForTest();
|
||||
_reloadHeld = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> TryConnectClientAsync(int timeoutMs)
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
using var timeout = new CancellationTokenSource(timeoutMs);
|
||||
await client.ConnectAsync(IPAddress.Loopback, _server.Port, timeout.Token);
|
||||
await using var stream = client.GetStream();
|
||||
var buffer = new byte[1];
|
||||
try
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer.AsMemory(0, 1), timeout.Token);
|
||||
return read > 0;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
ReleaseReloadLock();
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class SubListAsyncCacheSweepTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Cache_sweep_runs_async_and_prunes_stale_entries_without_write_locking_match_path()
|
||||
{
|
||||
using var sl = new SubList();
|
||||
sl.Insert(new Subscription { Subject = ">", Sid = "all" });
|
||||
|
||||
for (var i = 0; i < 1500; i++)
|
||||
_ = sl.Match($"orders.{i}");
|
||||
|
||||
var initial = sl.CacheCount;
|
||||
initial.ShouldBeGreaterThan(1024);
|
||||
|
||||
await sl.TriggerCacheSweepAsyncForTest();
|
||||
sl.CacheCount.ShouldBeLessThan(initial);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class SubListHighFanoutOptimizationTests
|
||||
{
|
||||
[Fact]
|
||||
public void High_fanout_nodes_enable_packed_list_optimization()
|
||||
{
|
||||
using var sl = new SubList();
|
||||
for (var i = 0; i < 300; i++)
|
||||
{
|
||||
sl.Insert(new Subscription
|
||||
{
|
||||
Subject = "orders.created",
|
||||
Sid = i.ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
sl.HighFanoutNodeCountForTest.ShouldBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
16
tests/NATS.Server.Tests/SubList/SubListMatchBytesTests.cs
Normal file
16
tests/NATS.Server.Tests/SubList/SubListMatchBytesTests.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class SubListMatchBytesTests
|
||||
{
|
||||
[Fact]
|
||||
public void MatchBytes_matches_subject_without_string_allocation_and_respects_remote_filter()
|
||||
{
|
||||
using var sl = new SubList();
|
||||
sl.MatchBytes("orders.created"u8).PlainSubs.Length.ShouldBe(0);
|
||||
|
||||
sl.Insert(new Subscription { Subject = "orders.*", Sid = "1" });
|
||||
sl.MatchBytes("orders.created"u8).PlainSubs.Length.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
24
tests/NATS.Server.Tests/SubList/SubListNotificationTests.cs
Normal file
24
tests/NATS.Server.Tests/SubList/SubListNotificationTests.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class SubListNotificationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Interest_change_notifications_are_emitted_for_local_and_remote_changes()
|
||||
{
|
||||
using var sl = new SubList();
|
||||
var changes = new List<InterestChange>();
|
||||
sl.InterestChanged += changes.Add;
|
||||
|
||||
var sub = new Subscription { Subject = "orders.created", Sid = "1" };
|
||||
sl.Insert(sub);
|
||||
sl.Remove(sub);
|
||||
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "r1", "A"));
|
||||
sl.ApplyRemoteSub(RemoteSubscription.Removal("orders.*", null, "r1", "A"));
|
||||
|
||||
changes.Count.ShouldBe(4);
|
||||
changes.Select(c => c.Kind).ShouldContain(InterestChangeKind.LocalAdded);
|
||||
changes.Select(c => c.Kind).ShouldContain(InterestChangeKind.RemoteAdded);
|
||||
}
|
||||
}
|
||||
17
tests/NATS.Server.Tests/SubList/SubListQueueWeightTests.cs
Normal file
17
tests/NATS.Server.Tests/SubList/SubListQueueWeightTests.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class SubListQueueWeightTests
|
||||
{
|
||||
[Fact]
|
||||
public void Remote_queue_weight_expands_matches()
|
||||
{
|
||||
using var sl = new SubList();
|
||||
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", "q", "r1", "A", QueueWeight: 3));
|
||||
|
||||
var matches = sl.MatchRemote("A", "orders.created");
|
||||
matches.Count.ShouldBe(3);
|
||||
matches.ShouldAllBe(m => m.Queue == "q");
|
||||
}
|
||||
}
|
||||
18
tests/NATS.Server.Tests/SubList/SubListRemoteFilterTests.cs
Normal file
18
tests/NATS.Server.Tests/SubList/SubListRemoteFilterTests.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class SubListRemoteFilterTests
|
||||
{
|
||||
[Fact]
|
||||
public void Match_remote_filters_by_account_and_subject()
|
||||
{
|
||||
using var sl = new SubList();
|
||||
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "r1", "A"));
|
||||
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "r2", "B"));
|
||||
|
||||
var aMatches = sl.MatchRemote("A", "orders.created");
|
||||
aMatches.Count.ShouldBe(1);
|
||||
aMatches[0].Account.ShouldBe("A");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user