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:
Joseph Doherty
2026-03-12 14:09:23 -04:00
parent 79c1ee8776
commit c30e67a69d
226 changed files with 17801 additions and 709 deletions

View 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;
}
}
}

View 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";
}

View 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();
}
}