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,117 @@
using NATS.Server.Auth;
using NATS.Server.Imports;
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests.Auth;
public class AccountResponseAndInterestParityBatch1Tests
{
[Fact]
public void ClientInfoHdr_constant_matches_go_value()
{
Account.ClientInfoHdr.ShouldBe("Nats-Request-Info");
}
[Fact]
public void Interest_and_subscription_interest_count_plain_and_queue_matches()
{
using var account = new Account("A");
account.SubList.Insert(new Subscription { Subject = "orders.*", Sid = "1" });
account.SubList.Insert(new Subscription { Subject = "orders.*", Sid = "2", Queue = "workers" });
account.Interest("orders.created").ShouldBe(2);
account.SubscriptionInterest("orders.created").ShouldBeTrue();
account.SubscriptionInterest("payments.created").ShouldBeFalse();
}
[Fact]
public void NumServiceImports_counts_distinct_from_subject_keys()
{
using var importer = new Account("importer");
using var exporter = new Account("exporter");
importer.Imports.AddServiceImport(new ServiceImport
{
DestinationAccount = exporter,
From = "svc.a",
To = "svc.remote.a",
});
importer.Imports.AddServiceImport(new ServiceImport
{
DestinationAccount = exporter,
From = "svc.a",
To = "svc.remote.b",
});
importer.Imports.AddServiceImport(new ServiceImport
{
DestinationAccount = exporter,
From = "svc.b",
To = "svc.remote.c",
});
importer.NumServiceImports().ShouldBe(2);
}
[Fact]
public void NumPendingResponses_filters_by_service_export()
{
using var account = new Account("A");
account.AddServiceExport("svc.one", ServiceResponseType.Singleton, null);
account.AddServiceExport("svc.two", ServiceResponseType.Singleton, null);
var seOne = account.Exports.Services["svc.one"];
var seTwo = account.Exports.Services["svc.two"];
account.Exports.Responses["r1"] = new ServiceImport
{
DestinationAccount = account,
From = "_R_.AAA.>",
To = "reply.one",
Export = seOne,
IsResponse = true,
};
account.Exports.Responses["r2"] = new ServiceImport
{
DestinationAccount = account,
From = "_R_.BBB.>",
To = "reply.two",
Export = seOne,
IsResponse = true,
};
account.Exports.Responses["r3"] = new ServiceImport
{
DestinationAccount = account,
From = "_R_.CCC.>",
To = "reply.three",
Export = seTwo,
IsResponse = true,
};
account.NumPendingAllResponses().ShouldBe(3);
account.NumPendingResponses("svc.one").ShouldBe(2);
account.NumPendingResponses("svc.two").ShouldBe(1);
account.NumPendingResponses("svc.unknown").ShouldBe(0);
}
[Fact]
public void RemoveRespServiceImport_removes_mapping_for_specified_reason()
{
using var account = new Account("A");
account.AddServiceExport("svc.one", ServiceResponseType.Singleton, null);
var seOne = account.Exports.Services["svc.one"];
var responseSi = new ServiceImport
{
DestinationAccount = account,
From = "_R_.ZZZ.>",
To = "reply",
Export = seOne,
IsResponse = true,
};
account.Exports.Responses["r1"] = responseSi;
account.RemoveRespServiceImport(responseSi, ResponseServiceImportRemovalReason.Timeout);
account.Exports.Responses.Count.ShouldBe(0);
}
}

View File

@@ -0,0 +1,46 @@
using NATS.Server.Auth;
namespace NATS.Server.Tests.Auth;
public class AuthModelAndCalloutConstantsParityTests
{
[Fact]
public void NkeyUser_exposes_parity_fields()
{
var now = DateTimeOffset.UtcNow;
var nkeyUser = new NKeyUser
{
Nkey = "UABC",
Issued = now,
AllowedConnectionTypes = new HashSet<string> { "STANDARD", "WEBSOCKET" },
ProxyRequired = true,
};
nkeyUser.Issued.ShouldBe(now);
nkeyUser.ProxyRequired.ShouldBeTrue();
nkeyUser.AllowedConnectionTypes.ShouldContain("STANDARD");
}
[Fact]
public void User_exposes_parity_fields()
{
var user = new User
{
Username = "alice",
Password = "secret",
AllowedConnectionTypes = new HashSet<string> { "STANDARD" },
ProxyRequired = false,
};
user.ProxyRequired.ShouldBeFalse();
user.AllowedConnectionTypes.ShouldContain("STANDARD");
}
[Fact]
public void External_auth_callout_constants_match_go_subjects_and_header()
{
ExternalAuthCalloutAuthenticator.AuthCalloutSubject.ShouldBe("$SYS.REQ.USER.AUTH");
ExternalAuthCalloutAuthenticator.AuthRequestSubject.ShouldBe("nats-authorization-request");
ExternalAuthCalloutAuthenticator.AuthRequestXKeyHeader.ShouldBe("Nats-Server-Xkey");
}
}

View File

@@ -0,0 +1,89 @@
using NATS.NKeys;
using NATS.Server.Auth;
using NATS.Server.Protocol;
namespace NATS.Server.Tests.Auth;
public class AuthServiceParityBatch4Tests
{
[Fact]
public void Build_assigns_global_account_to_orphan_users()
{
var service = AuthService.Build(new NatsOptions
{
Users = [new User { Username = "alice", Password = "secret" }],
});
var result = service.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "alice", Password = "secret" },
Nonce = [],
});
result.ShouldNotBeNull();
result.AccountName.ShouldBe(Account.GlobalAccountName);
}
[Fact]
public void Build_assigns_global_account_to_orphan_nkeys()
{
using var kp = KeyPair.CreatePair(PrefixByte.User);
var pub = kp.GetPublicKey();
var nonce = "test-nonce"u8.ToArray();
var sig = new byte[64];
kp.Sign(nonce, sig);
var service = AuthService.Build(new NatsOptions
{
NKeys = [new NKeyUser { Nkey = pub }],
});
var result = service.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions
{
Nkey = pub,
Sig = Convert.ToBase64String(sig),
},
Nonce = nonce,
});
result.ShouldNotBeNull();
result.AccountName.ShouldBe(Account.GlobalAccountName);
}
[Fact]
public void Build_validates_response_permissions_defaults_and_publish_allow()
{
var service = AuthService.Build(new NatsOptions
{
Users =
[
new User
{
Username = "alice",
Password = "secret",
Permissions = new Permissions
{
Response = new ResponsePermission { MaxMsgs = 0, Expires = TimeSpan.Zero },
},
},
],
});
var result = service.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "alice", Password = "secret" },
Nonce = [],
});
result.ShouldNotBeNull();
result.Permissions.ShouldNotBeNull();
result.Permissions.Response.ShouldNotBeNull();
result.Permissions.Response.MaxMsgs.ShouldBe(NatsProtocol.DefaultAllowResponseMaxMsgs);
result.Permissions.Response.Expires.ShouldBe(NatsProtocol.DefaultAllowResponseExpiration);
result.Permissions.Publish.ShouldNotBeNull();
result.Permissions.Publish.Allow.ShouldNotBeNull();
result.Permissions.Publish.Allow.Count.ShouldBe(0);
}
}

View File

@@ -0,0 +1,65 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using NATS.Server.Auth;
namespace NATS.Server.Tests.Auth;
public class TlsMapAuthParityBatch1Tests
{
[Fact]
public void GetTlsAuthDcs_extracts_domain_components_from_subject()
{
using var cert = CreateSelfSignedCert("CN=alice,DC=example,DC=com");
TlsMapAuthenticator.GetTlsAuthDcs(cert.SubjectName).ShouldBe("DC=example,DC=com");
}
[Fact]
public void DnsAltNameLabels_and_matches_follow_rfc6125_shape()
{
var labels = TlsMapAuthenticator.DnsAltNameLabels("*.Example.COM");
labels.ShouldBe(["*", "example", "com"]);
TlsMapAuthenticator.DnsAltNameMatches(labels, [new Uri("nats://node.example.com:6222")]).ShouldBeTrue();
TlsMapAuthenticator.DnsAltNameMatches(labels, [new Uri("nats://a.b.example.com:6222")]).ShouldBeFalse();
}
[Fact]
public void Authenticate_can_match_user_from_email_or_dns_san()
{
using var cert = CreateSelfSignedCertWithSan("CN=ignored", "ops@example.com", "router.example.com");
var auth = new TlsMapAuthenticator([
new User { Username = "ops@example.com", Password = "" },
new User { Username = "router.example.com", Password = "" },
]);
var ctx = new ClientAuthContext
{
Opts = new Protocol.ClientOptions(),
Nonce = [],
ClientCertificate = cert,
};
var result = auth.Authenticate(ctx);
result.ShouldNotBeNull();
(result.Identity == "ops@example.com" || result.Identity == "router.example.com").ShouldBeTrue();
}
private static X509Certificate2 CreateSelfSignedCert(string subjectName)
{
using var rsa = RSA.Create(2048);
var req = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));
}
private static X509Certificate2 CreateSelfSignedCertWithSan(string subjectName, string email, string dns)
{
using var rsa = RSA.Create(2048);
var req = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var sans = new SubjectAlternativeNameBuilder();
sans.AddEmailAddress(email);
sans.AddDnsName(dns);
req.CertificateExtensions.Add(sans.Build());
return req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));
}
}