Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
This commit is contained in:
54
src/NATS.Server/Server/RateCounter.cs
Normal file
54
src/NATS.Server/Server/RateCounter.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace NATS.Server.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Port of Go's rateCounter utility used for non-blocking allow/deny checks
|
||||
/// over a fixed one-second interval.
|
||||
/// Go reference: server/rate_counter.go
|
||||
/// </summary>
|
||||
public sealed class RateCounter
|
||||
{
|
||||
private readonly long _limit;
|
||||
private long _count;
|
||||
private ulong _blocked;
|
||||
private DateTime _end;
|
||||
private readonly TimeSpan _interval = TimeSpan.FromSeconds(1);
|
||||
private readonly Lock _mu = new();
|
||||
|
||||
public RateCounter(long limit)
|
||||
{
|
||||
_limit = Math.Max(1, limit);
|
||||
}
|
||||
|
||||
public bool Allow()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
lock (_mu)
|
||||
{
|
||||
if (now > _end)
|
||||
{
|
||||
_count = 0;
|
||||
_end = now + _interval;
|
||||
}
|
||||
else
|
||||
{
|
||||
_count++;
|
||||
}
|
||||
|
||||
var allow = _count < _limit;
|
||||
if (!allow)
|
||||
_blocked++;
|
||||
|
||||
return allow;
|
||||
}
|
||||
}
|
||||
|
||||
public ulong CountBlocked()
|
||||
{
|
||||
lock (_mu)
|
||||
{
|
||||
var blocked = _blocked;
|
||||
_blocked = 0;
|
||||
return blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/NATS.Server/Server/ServerErrorConstants.cs
Normal file
27
src/NATS.Server/Server/ServerErrorConstants.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace NATS.Server.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Error string constants mirrored from Go server/errors.go.
|
||||
/// These are kept as literals for parity and can be used by validation
|
||||
/// paths that currently surface generic exceptions.
|
||||
/// </summary>
|
||||
public static class ServerErrorConstants
|
||||
{
|
||||
public const string ErrBadQualifier = "bad qualifier";
|
||||
public const string ErrTooManyAccountConnections = "maximum account active connections exceeded";
|
||||
public const string ErrTooManySubs = "maximum subscriptions exceeded";
|
||||
public const string ErrTooManySubTokens = "subject has exceeded number of tokens limit";
|
||||
public const string ErrReservedAccount = "reserved account";
|
||||
public const string ErrMissingService = "service missing";
|
||||
public const string ErrBadServiceType = "bad service response type";
|
||||
public const string ErrBadSampling = "bad sampling percentage, should be 1-100";
|
||||
public const string ErrAccountResolverUpdateTooSoon = "account resolver update too soon";
|
||||
public const string ErrAccountResolverSameClaims = "account resolver no new claims";
|
||||
public const string ErrStreamImportAuthorization = "stream import not authorized";
|
||||
public const string ErrStreamImportBadPrefix = "stream import prefix can not contain wildcard tokens";
|
||||
public const string ErrStreamImportDuplicate = "stream import already exists";
|
||||
public const string ErrServiceImportAuthorization = "service import not authorized";
|
||||
public const string ErrImportFormsCycle = "import forms a cycle";
|
||||
public const string ErrCycleSearchDepth = "search cycle depth exhausted";
|
||||
public const string ErrNoTransforms = "no matching transforms available";
|
||||
}
|
||||
82
src/NATS.Server/Server/ServerUtilities.cs
Normal file
82
src/NATS.Server/Server/ServerUtilities.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NATS.Server.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Misc utility helpers ported from Go's server/util.go.
|
||||
/// </summary>
|
||||
public static class ServerUtilities
|
||||
{
|
||||
private static readonly Regex UrlAuthRegex = new(
|
||||
@"^(?<scheme>[a-zA-Z][a-zA-Z0-9+\-.]*://)(?<user>[^:@/]+):(?<pass>[^@/]+)@(?<rest>.+)$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a host/port string with a default port fallback.
|
||||
/// Mirrors util.go parseHostPort behavior where 0/-1 port values fall back.
|
||||
/// </summary>
|
||||
public static (string Host, int Port) ParseHostPort(string hostPort, int defaultPort)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hostPort))
|
||||
throw new ArgumentException("no hostport specified", nameof(hostPort));
|
||||
|
||||
var input = hostPort.Trim();
|
||||
|
||||
if (input.StartsWith('['))
|
||||
{
|
||||
var endBracket = input.IndexOf(']');
|
||||
if (endBracket < 0)
|
||||
throw new FormatException($"Invalid host:port '{hostPort}'");
|
||||
|
||||
var host = input[1..endBracket].Trim();
|
||||
if (endBracket + 1 >= input.Length || input[endBracket + 1] != ':')
|
||||
return (host, defaultPort);
|
||||
|
||||
var portText = input[(endBracket + 2)..].Trim();
|
||||
if (!int.TryParse(portText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ipv6Port))
|
||||
throw new FormatException($"Invalid host:port '{hostPort}'");
|
||||
if (ipv6Port == 0 || ipv6Port == -1)
|
||||
ipv6Port = defaultPort;
|
||||
return (host, ipv6Port);
|
||||
}
|
||||
|
||||
var colonIdx = input.LastIndexOf(':');
|
||||
if (colonIdx < 0)
|
||||
return (input, defaultPort);
|
||||
|
||||
var parsedHost = input[..colonIdx].Trim();
|
||||
var parsedPortText = input[(colonIdx + 1)..].Trim();
|
||||
|
||||
if (parsedPortText.Length == 0)
|
||||
return (parsedHost, defaultPort);
|
||||
|
||||
if (!int.TryParse(parsedPortText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedPort))
|
||||
throw new FormatException($"Invalid host:port '{hostPort}'");
|
||||
|
||||
if (parsedPort == 0 || parsedPort == -1)
|
||||
parsedPort = defaultPort;
|
||||
|
||||
return (parsedHost, parsedPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redacts password in a single URL user-info section.
|
||||
/// </summary>
|
||||
public static string RedactUrlString(string url)
|
||||
{
|
||||
var match = UrlAuthRegex.Match(url);
|
||||
if (!match.Success)
|
||||
return url;
|
||||
|
||||
return $"{match.Groups["scheme"].Value}{match.Groups["user"].Value}:xxxxx@{match.Groups["rest"].Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redacts URL credentials for a URL list.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> RedactUrlList(IEnumerable<string> urls)
|
||||
{
|
||||
return urls.Select(RedactUrlString).ToArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user